Add basic library code to manage projects, activities and times.
This commit is contained in:
parent
529efb3668
commit
fb576f3d51
7 changed files with 663 additions and 0 deletions
160
lib/times.go
Normal file
160
lib/times.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Time entity as defined by the specification.
|
||||
type Time struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project"`
|
||||
ActivityID string `json:"activity"`
|
||||
Start time.Time `json:"start"`
|
||||
End time.Time `json:"end"`
|
||||
}
|
||||
|
||||
// SetTime persists a time entry.
|
||||
func SetTime(entry *Time) error {
|
||||
// Traverse range by day (in case is spans multiple days
|
||||
for date := entry.Start.Truncate(24 * time.Hour); !date.After(entry.End); date = date.AddDate(0, 0, 1) {
|
||||
start := entry.Start
|
||||
if date.After(start) {
|
||||
start = date
|
||||
}
|
||||
end := entry.End
|
||||
if date.AddDate(0, 0, 1).Add(-1 * time.Second).Before(end) {
|
||||
end = date.AddDate(0, 0, 1).Add(-1 * time.Second)
|
||||
}
|
||||
|
||||
dayEntry := Time{
|
||||
ProjectID: entry.ProjectID,
|
||||
ActivityID: entry.ActivityID,
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
|
||||
uuid, uuidErr := newUUID()
|
||||
if uuidErr != nil {
|
||||
return uuidErr
|
||||
}
|
||||
|
||||
year, month, day := date.UTC().Date()
|
||||
file, fileErr := getTimeFile(year, int(month), day, uuid)
|
||||
if fileErr != nil {
|
||||
return fileErr
|
||||
}
|
||||
|
||||
writeErr := writeTime(&dayEntry, file)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readTimeRange reads time entries for a given time range.
|
||||
func readTimeRange(start, end time.Time) ([]*Time, error) {
|
||||
entries := []*Time{}
|
||||
|
||||
// Traverse range by day
|
||||
for date := start; !date.After(end); date = date.AddDate(0, 0, 1) {
|
||||
// Traverse files
|
||||
year, month, day := date.UTC().Date()
|
||||
folder, folderErr := getTimeFolder(year, int(month), day)
|
||||
if folderErr != nil {
|
||||
return nil, folderErr
|
||||
}
|
||||
|
||||
slog.Debug(fmt.Sprintf("Read time files from folder %s", folder))
|
||||
files, filesErr := os.ReadDir(folder)
|
||||
if errors.Is(filesErr, fs.ErrNotExist) {
|
||||
return entries, nil
|
||||
}
|
||||
if filesErr != nil {
|
||||
return nil, filesErr
|
||||
}
|
||||
for _, file := range files {
|
||||
entry, entryErr := readTime(filepath.Join(folder, file.Name()))
|
||||
if entryErr != nil {
|
||||
return nil, entryErr
|
||||
}
|
||||
if (entry.Start.After(start) && entry.Start.Before(end)) || (entry.End.After(start) && entry.End.Before(end)) {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// readTime reads a time entry from a file and converts it into an entity instance.
|
||||
func readTime(filename string) (*Time, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
time := Time{}
|
||||
if err := json.NewDecoder(file).Decode(&time); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &time, nil
|
||||
}
|
||||
|
||||
// writeTime writes a time instance to a file.
|
||||
func writeTime(time *Time, filename string) error {
|
||||
dirErr := os.MkdirAll(filepath.Dir(filename), 0o750)
|
||||
if dirErr != nil {
|
||||
return dirErr
|
||||
}
|
||||
|
||||
file, fileErr := os.Create(filename)
|
||||
if fileErr != nil {
|
||||
return fileErr
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
jsonErr := encoder.Encode(time)
|
||||
if jsonErr != nil {
|
||||
return jsonErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTimeFolder returns the folder to store time entity files in.
|
||||
func getTimeFolder(year int, month int, day int) (string, error) {
|
||||
folder, err := getDataDir("times")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(folder, strconv.Itoa(year), strconv.Itoa(month), strconv.Itoa(day)), nil
|
||||
}
|
||||
|
||||
// getTimeFile returns the file name for a time entity instance.
|
||||
func getTimeFile(year int, month int, day int, name string) (string, error) {
|
||||
folder, err := getTimeFolder(year, month, day)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(folder, name+".json"), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue