package gotime import ( "errors" "fmt" "io" "io/ioutil" "os" "strings" "time" ) type GoTime struct { Dir string timers []Timer files []string timeFormat string Tags []string CurrDataFile string } // Create creates a new instance of GoTime // with the given data directory func Create(dir string) *GoTime { g := &GoTime{Dir: dir} g.timeFormat = "20060102T150405Z" g.CurrDataFile = time.Now().Format("2006-01.data") g.files = g.getTimerFiles() for _, f := range g.files { g.addTimersFromFile(f) } g.ResetIds() return g } func (g *GoTime) ResetIds() { // Now set IDs id := 1 for i := len(g.timers) - 1; i >= 0; i-- { g.timers[i].SetId(id) id++ } } // IsOn returns true if a timer is currently running func (g *GoTime) IsOn() bool { t := g.Status() if t.End.IsZero() { return true } return false } // Status returns the most recent timer func (g *GoTime) Status() *Timer { if len(g.timers) == 0 { return nil } return &g.timers[len(g.timers)-1] } // IsRunning returns if the Status() is running func (g *GoTime) IsRunning() bool { return g.Status().IsRunning() } func (g *GoTime) GetAllTimers() []Timer { return g.timers } func (g *GoTime) GetTimer(id int) (*Timer, error) { if len(g.timers) >= id { return &g.timers[len(g.timers)-id], nil } return nil, errors.New("Invalid Timer Id") } // getTimerFiles Returns a string slice of all of the data file names func (g *GoTime) getTimerFiles() []string { var ret []string // Timer files are all files in g.dir/data except undo.data files, _ := ioutil.ReadDir(g.Dir + "/data/") for _, f := range files { if f.Name() != "undo.data" { ret = append(ret, f.Name()) } } return ret } // addTimersFromFile Adds all timer lines from g.Dir/data/f and adds a timer for it func (g *GoTime) addTimersFromFile(f string) error { lines, err := g.readDataFile(f) if err != nil { return err } for i := range lines { t, err := CreateTimerFromString(lines[i]) if err != nil { return err } if !t.Beg.IsZero() { g.timers = append(g.timers, *t) for i := range t.Tags { g.AddTag(t.Tags[i]) } } } return err } // AddTag adds a tag to the list of all used tags func (g *GoTime) AddTag(tg string) { if !g.HasTag(tg) { g.Tags = append(g.Tags, tg) } } // HasTag returns if this tag is in our list func (g *GoTime) HasTag(tg string) bool { for i := range g.Tags { if g.Tags[i] == tg { return true } } return false } // Start starts a new timer. If one is already active, it stops it first func (g *GoTime) StartTimer() *Timer { if g.IsRunning() { g.StopTimer() } t := new(Timer) t.Beg = time.Now() g.timers = append(g.timers, *t) g.AddTimerToCurrentDataFile(t) g.ResetIds() return g.Status() } // Stop stops the currently active timer func (g *GoTime) StopTimer() *Timer { if g.IsRunning() { g.Status().Stop() } g.UpdateTimerInDataFile(1, g.Status()) return g.Status() } func (g *GoTime) RemoveTagsFromTimer(id int, tg []string) (*Timer, error) { tmr, err := g.GetTimer(id) if err != nil { return nil, err } for i := range tg { tmr.RemoveTag(tg[i]) } err = g.UpdateTimerInDataFile(id, tmr) if err != nil { return nil, err } return tmr, nil } func (g *GoTime) AddTagsToTimer(id int, tg []string) (*Timer, error) { tmr, err := g.GetTimer(id) if err != nil { return nil, err } for i := range tg { tmr.AddTag(tg[i]) } err = g.UpdateTimerInDataFile(id, tmr) if err != nil { return nil, err } return tmr, nil } // AddTag adds new tags to the most recent timer func (g *GoTime) AddTagsToCurrentTimer(tgs []string) (*Timer, error) { return g.AddTagsToTimer(1, tgs) } func (g *GoTime) AddTimerToCurrentDataFile(tmr *Timer) error { f, err := os.OpenFile(g.Dir+"/data/"+g.CurrDataFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return err } defer f.Close() _, err = f.WriteString(tmr.ToString()) return err } func (g *GoTime) UpdateTimerInDataFile(id int, tmr *Timer) error { var err error var datFileNm string var txn *Transaction // We have to step back through the files to find this id var lines []string var linesCnt int for i := len(g.files) - 1; i >= 0; i-- { lines, err = g.readDataFile(g.files[i]) if err != nil { return err } else { if linesCnt+len(lines) >= id { datFileNm = g.files[i] } } if datFileNm != "" { break } linesCnt += len(lines) } // datFileNm should be the right data file, linesCnt should be the starting index in that file // We have to find the line in the file that is (id-linesCnt) lines from the bottom // First back the file up, in case something messes up if err = g.backupDataFile(datFileNm); err != nil { return err } var f *os.File f, err = os.OpenFile(g.Dir+"/data/"+datFileNm, os.O_RDWR|os.O_TRUNC, 0666) for i := range lines { if linesCnt+(len(lines)-i) == id { // Found the timer line if _, err = f.WriteString(tmr.ToString() + "\n"); err != nil { break } var oldTmr *Timer oldTmr, err = CreateTimerFromString(lines[i]) txn = CreateTxn(oldTmr, tmr) } else { if _, err = f.WriteString(lines[i] + "\n"); err != nil { break } } } f.Close() if err != nil { // An error occurred, restore the backup if restErr := g.restoreBackupFile(datFileNm); restErr != nil { return errors.New(err.Error() + " (Backup Restore Error: " + restErr.Error() + ")") } return err } // No error, add the transaction to the undo file if undoErr := g.writeTxnToUndoFile(txn); undoErr != nil { return undoErr } return err } func (g *GoTime) readDataFile(fn string) ([]string, error) { var lines []string content, err := ioutil.ReadFile(g.Dir + "/data/" + fn) if err != nil { fmt.Println(err) return lines, err } cntString := strings.TrimSpace(string(content)) lines = strings.Split(cntString, "\n") return lines, err } func (g *GoTime) backupDataFile(fn string) error { var err error if _, err = os.Stat(g.Dir + "/data-backups"); err != nil { if err = os.Mkdir(g.Dir+"/data-backups", 0755); err != nil { return err } } return g.copyFile(g.Dir+"/data/"+fn, g.Dir+"/data-backups/"+fn) } func (g *GoTime) restoreBackupFile(fn string) error { var err error if _, err = os.Stat(g.Dir + "/data-backups"); err != nil { if err = os.Mkdir(g.Dir+"/data-backups", 0755); err != nil { return err } } return g.copyFile(g.Dir+"/data-backups/"+fn, g.Dir+"/data/"+fn) } func (g *GoTime) writeTxnToUndoFile(txn *Transaction) error { // TODO: For now we're ignoring the undo file. TimeWarrior doesn't support it yet /* // Backup the undo file var err error var f *os.File err = g.backupDataFile("undo.data") if err != nil { // Couldn't backup undo file... return err } f, err = os.OpenFile(g.Dir+"/data/undo.data", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return err } f.Close() if _, err = f.WriteString(txn.ToString()); err != nil { undoErr := g.restoreBackupFile("undo.data") if undoErr != nil { return errors.New(err.Error() + " (Backup Restore Error: " + undoErr.Error() + ")") } return err } */ return nil } func (g *GoTime) copyFile(src, dst string) (err error) { sfi, err := os.Stat(src) if err != nil { return } if !sfi.Mode().IsRegular() { // cannot copy non-regular files (e.g., directories, // symlinks, devices, etc.) return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String()) } dfi, err := os.Stat(dst) if err != nil { if !os.IsNotExist(err) { return } } else { if !(dfi.Mode().IsRegular()) { return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String()) } if os.SameFile(sfi, dfi) { return } } if err = os.Link(src, dst); err == nil { return } err = g.copyFileContents(src, dst) return } func (g *GoTime) copyFileContents(src, dst string) (err error) { in, err := os.Open(src) if err != nil { return } defer in.Close() out, err := os.Create(dst) if err != nil { return } defer func() { cerr := out.Close() if err == nil { err = cerr } }() if _, err = io.Copy(out, in); err != nil { return } err = out.Sync() return } func TagIsMultiword(tg string) bool { if strings.Contains(tg, " ") { return true } if strings.Contains(tg, "-") { return true } return false }