diff --git a/cmd/gime/.gitignore b/cmd/gime/.gitignore index 4dfebc1..1fdcae5 100644 --- a/cmd/gime/.gitignore +++ b/cmd/gime/.gitignore @@ -1 +1,2 @@ gime +*.db diff --git a/cmd/gime/README.md b/cmd/gime/README.md index 9f465fc..327f2eb 100644 --- a/cmd/gime/README.md +++ b/cmd/gime/README.md @@ -1,4 +1,4 @@ gime ==== -This folder contains the cli utility for gime +This folder contains the cli application for gime diff --git a/cmd/gime/main.go b/cmd/gime/main.go index 15740bf..a5b7282 100644 --- a/cmd/gime/main.go +++ b/cmd/gime/main.go @@ -15,10 +15,13 @@ const ( var validOperations map[string][]string var activeTimeEntry *gime.TimeEntry +var gdb *gime.GimeDB func main() { + var ret int initialize() var parms []string + if len(os.Args) > 1 { parms = os.Args[1:] parms[0] = matchParameter(parms[0]) @@ -29,17 +32,21 @@ func main() { switch parms[0] { case "help": - printHelp() + ret = printHelp() case "status": - printStatus() + ret = printStatus() case "start": - startTimer(parms[1:]) + ret = startTimer(parms[1:]) case "end", "stop": - stopTimer(parms[1:]) + ret = stopTimer(parms[1:]) + default: + fmt.Println("Unknown command") + ret = 1 } + os.Exit(ret) } -func printHelp() { +func printHelp() int { fmt.Println("gime - A simple timekeeping application\n") for _, v := range validOperations { for vi := range v { @@ -47,9 +54,10 @@ func printHelp() { } fmt.Println("") } + return 0 } -func printStatus() { +func printStatus() int { if activeTimeEntry == nil { fmt.Println("No timer running") } else { @@ -62,12 +70,19 @@ func printStatus() { } fmt.Println(time.Since(activeTimeEntry.GetStart()).String()) } + return 0 } -func startTimer(args []string) { +// startTimer takes a list of arguments and returns the return code +// to be passed along to os.Exit +func startTimer(args []string) int { var err error - var tm time.Time - st := time.Now() + tm := time.Now() + curr := gdb.LoadTimeEntryCollection(gime.TypeCurrent) + if curr.Length() > 0 { + // TODO: Stop all Timers in curr + } + tagStart := 0 if len(args) > 0 { // Check if the first argument looks like a date/time tm, err = time.Parse("15:04", args[0]) @@ -77,11 +92,29 @@ func startTimer(args []string) { } if err != nil { // Just start it now + tm = time.Now() + } else { + tagStart = 1 } - _, _ = tm, st + var entry *gime.TimeEntry + + var timerArgs []string + if tagStart < len(args) { + timerArgs = args[tagStart:] + } + fmt.Println(tm) + if entry, err = gime.CreateTimeEntry(tm, time.Time{}, timerArgs); err != nil { + fmt.Println(err) + return 1 + } + if err = gdb.SaveTimeEntry(entry); err != nil { + fmt.Println(err) + return 1 + } + return 0 } -func stopTimer(args []string) { +func stopTimer(args []string) int { var err error var tm time.Time st := time.Now() @@ -94,9 +127,16 @@ func stopTimer(args []string) { } _, _ = tm, st + return 0 } func initialize() { + var err error + if gdb, err = gime.LoadDatabase("./", "gime.db", "gimearch.db"); err != nil { + fmt.Println("Error loading the database") + os.Exit(1) + } + validOperations = make(map[string][]string) validOperations["status"] = []string{ "status - Print the current status of the timer", diff --git a/model.go b/model.go index 2178d26..ef2e60f 100644 --- a/model.go +++ b/model.go @@ -1 +1,131 @@ package gime + +import ( + "errors" + + "github.com/br0xen/boltease" +) + +type GimeDB struct { + db *boltease.DB + archDb *boltease.DB + dbOpened int + dbArchOpened int + path string + filename, arch string +} + +const ( + TypeCurrent = "current" + TypeRecent = "recent" + TypeArchive = "archive" +) + +// Load Database returns a database loaded off the files given +// name and archName located in path +func LoadDatabase(path, name, archName string) (*GimeDB, error) { + if path[len(path)-1] != '/' { + path = path + "/" + } + gdb := GimeDB{ + path: path, + filename: name, + arch: archName, + } + gdb.initDatabase() + return &gdb, nil +} + +func (gdb *GimeDB) openDatabase() error { + gdb.dbOpened += 1 + if gdb.dbOpened == 1 { + var err error + gdb.db, err = boltease.Create(gdb.path+gdb.filename, 0600, nil) + if err != nil { + return err + } + } + return nil +} + +func (gdb *GimeDB) closeDatabase() error { + gdb.dbOpened -= 1 + if gdb.dbOpened == 0 { + return gdb.db.CloseDB() + } + return nil +} + +func (gdb *GimeDB) openArchiveDatabase() error { + gdb.dbArchOpened += 1 + if gdb.dbArchOpened == 1 { + var err error + gdb.archDb, err = boltease.Create(gdb.path+gdb.arch, 0600, nil) + if err != nil { + return err + } + } + return nil +} + +func (gdb *GimeDB) closeArchiveDatabase() error { + gdb.dbArchOpened -= 1 + if gdb.dbArchOpened == 0 { + return gdb.archDb.CloseDB() + } + return nil +} + +func (gdb *GimeDB) initDatabase() error { + // Initialize the current/recent/settings database + var err error + if err = gdb.openDatabase(); err != nil { + return err + } + defer gdb.closeDatabase() + // Create the path to the bucket to store application settings + if err := gdb.db.MkBucketPath([]string{"settings"}); err != nil { + return err + } + // Create the path to the bucket to store the current time entry + if err := gdb.db.MkBucketPath([]string{TypeCurrent}); err != nil { + return err + } + // Create the path to the bucket to store recent time entries + if err := gdb.db.MkBucketPath([]string{TypeRecent}); err != nil { + return err + } + + // Now initialize the Archive Database + if err = gdb.openArchiveDatabase(); err != nil { + return err + } + defer gdb.closeArchiveDatabase() + // Create the path to the bucket to store archived time entries + return gdb.archDb.MkBucketPath([]string{TypeArchive}) +} + +func (gdb *GimeDB) openDBType(tp string) (*boltease.DB, error) { + var err error + if tp == TypeCurrent || tp == TypeRecent { + if err = gdb.openDatabase(); err != nil { + return nil, err + } + return gdb.db, err + } else if tp == TypeArchive { + if err = gdb.openArchiveDatabase(); err != nil { + return nil, err + } + return gdb.archDb, err + } + return nil, errors.New("Invalid database type: " + tp) +} + +func (gdb *GimeDB) closeDBType(tp string) error { + if tp == TypeCurrent || tp == TypeRecent { + return gdb.closeDatabase() + } else if tp == TypeArchive { + return gdb.closeArchiveDatabase() + } + return errors.New("Invalid database type: " + tp) +} diff --git a/model_timeentry.go b/model_timeentry.go new file mode 100644 index 0000000..a9640e7 --- /dev/null +++ b/model_timeentry.go @@ -0,0 +1,104 @@ +package gime + +import ( + "errors" + "strconv" + "time" + + "github.com/br0xen/boltease" +) + +// SaveTimeEntry creates a time entry in the database +// If TimeEntry.end is zero, then it puts it in TypeCurrent +func (gdb *GimeDB) SaveTimeEntry(te *TimeEntry) error { + var err error + var useDb *boltease.DB + tp := TypeRecent + if te.end.IsZero() { + // Currently running + tp = TypeCurrent + useDb, err = gdb.openDBType(tp) + if err != nil { + return err + } + } else { + // We have an end time. Does this entry go in 'recent' or 'archive' + // We shove times that happened 30 days ago into 'archive' + if time.Since(te.end) > (time.Hour * 24 * 30) { + tp = TypeArchive + } + } + tePath := []string{tp, te.uuid} + if err = useDb.SetTimestamp(tePath, "start", te.start); err != nil { + return err + } + if err = useDb.SetTimestamp(tePath, "end", te.end); err != nil { + return err + } + for i := range te.tags { + err = useDb.SetValue(append(tePath, "tags"), strconv.Itoa(i), te.tags[i]) + } + return nil +} + +// dbGetAllTimeEntries gets all time entries of a specific type +// tp can be: +// TypeCurrent = "current" +// TypeRecent = "recent" +// TypeArchive = "archive" +// Getting all archived time entries has the potential to be a lot of data +func (gdb *GimeDB) dbGetAllTimeEntries(tp string) []TimeEntry { + var ret []TimeEntry + var useDb *boltease.DB + var err error + if useDb, err = gdb.openDBType(tp); err != nil { + return ret + } + defer gdb.closeDBType(tp) + + var uuids []string + if uuids, err = useDb.GetBucketList([]string{tp}); err != nil { + return ret + } + for _, v := range uuids { + if te, _ := gdb.dbGetTimeEntry(tp, v); te != nil { + ret = append(ret, *te) + } + } + return ret +} + +// dbGetTimeEntry pulls a time entry of type tp with the given id +// from the db and returns it. +func (gdb *GimeDB) dbGetTimeEntry(tp, id string) (*TimeEntry, error) { + var ret *TimeEntry + var err error + var useDb *boltease.DB + + if useDb, err = gdb.openDBType(tp); err != nil { + return ret, err + } + defer gdb.closeDBType(tp) + + entryPath := []string{tp, id} + ret = new(TimeEntry) + ret.uuid = id + if ret.start, err = useDb.GetTimestamp(entryPath, "start"); err != nil { + return nil, errors.New("Unable to read entry start time") + } + if ret.end, err = useDb.GetTimestamp(entryPath, "end"); err != nil { + return nil, errors.New("Unable to read entry end time") + } + var keys []string + entryTagsPath := append(entryPath, "tags") + if keys, err = useDb.GetKeyList(entryTagsPath); err != nil { + keys = []string{} + } + for i := range keys { + var val string + if val, err = useDb.GetValue(entryTagsPath, keys[i]); err != nil { + ret.tags = append(ret.tags, val) + } + } + return ret, nil +} diff --git a/timeentry.go b/timeentry.go index eb7357a..f264e2b 100644 --- a/timeentry.go +++ b/timeentry.go @@ -19,7 +19,7 @@ type TimeEntry struct { // An error is returned if no start time is given or if the end time given is // non-zero and earlier than the start time. func CreateTimeEntry(s, e time.Time, t []string) (*TimeEntry, error) { - var ret *TimeEntry + ret := new(TimeEntry) ret.uuid = uuid.New() if s.IsZero() { // No start time given, return error diff --git a/timeentry_collection.go b/timeentry_collection.go index 1e42871..33f623a 100644 --- a/timeentry_collection.go +++ b/timeentry_collection.go @@ -7,6 +7,16 @@ type TimeEntryCollection struct { list []TimeEntry } +// Load a TimeEntry collection from a database +func (gdb *GimeDB) LoadTimeEntryCollection(tp string) *TimeEntryCollection { + ret := new(TimeEntryCollection) + entries := gdb.dbGetAllTimeEntries(tp) + for i := range entries { + ret.Push(&entries[i]) + } + return ret +} + // Length returns how many time entries are in the collection func (tc *TimeEntryCollection) Length() int { return len(tc.list)