396 lines
8.2 KiB
Go
396 lines
8.2 KiB
Go
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
|
|
}
|