timefiles/lib/times.go

160 lines
3.6 KiB
Go

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
}