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
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module suruatoel.xyz/timefiles
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
121
lib/activities.go
Normal file
121
lib/activities.go
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Activity entity as defined by the specification.
|
||||||
|
type Activity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActivities retrieves all stored activity instances of a project.
|
||||||
|
func GetActivities(project *Project) ([]*Activity, error) {
|
||||||
|
folder, folderErr := getActivitiesFolder(project.ID)
|
||||||
|
if folderErr != nil {
|
||||||
|
return nil, folderErr
|
||||||
|
}
|
||||||
|
|
||||||
|
dirErr := os.MkdirAll(folder, 0o750)
|
||||||
|
if dirErr != nil {
|
||||||
|
return nil, dirErr
|
||||||
|
}
|
||||||
|
|
||||||
|
files, filesErr := os.ReadDir(folder)
|
||||||
|
if filesErr != nil {
|
||||||
|
return nil, filesErr
|
||||||
|
}
|
||||||
|
|
||||||
|
activities := []*Activity{}
|
||||||
|
for _, file := range files {
|
||||||
|
activity, fileErr := readActivity(filepath.Join(folder, file.Name()))
|
||||||
|
if fileErr != nil {
|
||||||
|
return nil, fileErr
|
||||||
|
}
|
||||||
|
activities = append(activities, activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetActivity persists an activity.
|
||||||
|
func SetActivity(project *Project, activity *Activity) error {
|
||||||
|
uuid, uuidErr := newUUID()
|
||||||
|
if uuidErr != nil {
|
||||||
|
return uuidErr
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.ID = uuid
|
||||||
|
|
||||||
|
file, fileErr := getActivityFile(project.ID, uuid)
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeActivity(file, activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readActivity reads an activity from a file and converts it into an entity instance.
|
||||||
|
func readActivity(filename string) (*Activity, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
project := Activity{}
|
||||||
|
if err := json.NewDecoder(file).Decode(&project); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeActivity writes an activity instance to a file.
|
||||||
|
func writeActivity(filename string, activity *Activity) error {
|
||||||
|
dirErr := os.MkdirAll(filepath.Dir(filename), 0o750)
|
||||||
|
if dirErr != nil {
|
||||||
|
return dirErr
|
||||||
|
}
|
||||||
|
fmt.Println("success")
|
||||||
|
|
||||||
|
file, fileErr := os.Create(filename)
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
jsonErr := encoder.Encode(activity)
|
||||||
|
if jsonErr != nil {
|
||||||
|
return jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getActivitiesFolder returns the folder to store activity entity files in.
|
||||||
|
func getActivitiesFolder(projectID string) (string, error) {
|
||||||
|
folder, err := getDataDir("activities")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(folder, projectID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getActivityFile returns the file name for an activity entity instance.
|
||||||
|
func getActivityFile(projectID string, name string) (string, error) {
|
||||||
|
folder, err := getActivitiesFolder(projectID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(folder, name+".json"), nil
|
||||||
|
}
|
||||||
39
lib/common.go
Normal file
39
lib/common.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Package lib provides routines for creating and reading data files as defined by the specification.
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dataDir holds the configured directory to read/write data from/to.
|
||||||
|
var dataDir string
|
||||||
|
|
||||||
|
// SetDataDir sets the global directory to read/write data from/to. It is typically called once at application start.
|
||||||
|
func SetDataDir(directory string) {
|
||||||
|
dataDir = directory
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDataDir gets the global directory to read/write data from/to for a schema entity.
|
||||||
|
func getDataDir(entity string) (string, error) {
|
||||||
|
if dataDir != "" {
|
||||||
|
return filepath.Join(dataDir, entity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
home, homeErr := os.UserHomeDir()
|
||||||
|
if homeErr != nil {
|
||||||
|
return "", homeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(home, ".protime", entity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newUUID creates a new UUID for an entity instance using the OS command `uuidgen`.
|
||||||
|
func newUUID() (string, error) {
|
||||||
|
out, err := exec.Command("uuidgen").Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out[:len(out)-1]), nil
|
||||||
|
}
|
||||||
142
lib/projects.go
Normal file
142
lib/projects.go
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Project entity. In addition to the JSON properties as defined by the specification, it holds activities of the
|
||||||
|
// project organized by title and by ID in a map.
|
||||||
|
type Project struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
activitiesByTitle map[string]*Activity
|
||||||
|
activitiesByID map[string]*Activity
|
||||||
|
activitiesLoaded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProject creates a new project instance.
|
||||||
|
func NewProject() *Project {
|
||||||
|
return &Project{
|
||||||
|
activitiesByID: make(map[string]*Activity),
|
||||||
|
activitiesByTitle: make(map[string]*Activity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjects retrieves all stored project instances. It does not load related data like activities or times.
|
||||||
|
func GetProjects() ([]*Project, error) {
|
||||||
|
folder, folderErr := getProjectFolder()
|
||||||
|
if folderErr != nil {
|
||||||
|
return nil, folderErr
|
||||||
|
}
|
||||||
|
|
||||||
|
dirErr := os.MkdirAll(folder, 0o750)
|
||||||
|
if dirErr != nil {
|
||||||
|
return nil, dirErr
|
||||||
|
}
|
||||||
|
|
||||||
|
files, filesErr := os.ReadDir(folder)
|
||||||
|
if filesErr != nil {
|
||||||
|
return nil, filesErr
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := []*Project{}
|
||||||
|
for _, file := range files {
|
||||||
|
project, fileErr := readProject(filepath.Join(folder, file.Name()))
|
||||||
|
if fileErr != nil {
|
||||||
|
return nil, fileErr
|
||||||
|
}
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject retrieves a single project by its ID. It does not load related data like activities or times.
|
||||||
|
func GetProject(id string) (*Project, error) {
|
||||||
|
file, fileErr := getProjectFile(id)
|
||||||
|
if fileErr != nil {
|
||||||
|
return nil, fileErr
|
||||||
|
}
|
||||||
|
project, err := readProject(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
project.ID = id
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProject persists a project.
|
||||||
|
func SetProject(project *Project) error {
|
||||||
|
uuid, uuidErr := newUUID()
|
||||||
|
if uuidErr != nil {
|
||||||
|
return uuidErr
|
||||||
|
}
|
||||||
|
|
||||||
|
project.ID = uuid
|
||||||
|
|
||||||
|
file, fileErr := getProjectFile(uuid)
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeProject(file, project)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readProject reads a project from a file and converts it into an entity instance.
|
||||||
|
func readProject(filename string) (*Project, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
project := NewProject()
|
||||||
|
if err := json.NewDecoder(file).Decode(&project); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeProject writes a project instance to a file.
|
||||||
|
func writeProject(filename string, project *Project) 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(project)
|
||||||
|
if jsonErr != nil {
|
||||||
|
return jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProjectFolder returns the folder to store project entity files in.
|
||||||
|
func getProjectFolder() (string, error) {
|
||||||
|
return getDataDir("projects")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProjectFile returns the file name for a project entity instance.
|
||||||
|
func getProjectFile(name string) (string, error) {
|
||||||
|
folder, err := getProjectFolder()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(folder, name+".json"), nil
|
||||||
|
}
|
||||||
81
lib/stats.go
Normal file
81
lib/stats.go
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TotalTime represents the time spent per project and activity including a total summary.
|
||||||
|
type TotalTime struct {
|
||||||
|
Totals []*TotalTimeEntry
|
||||||
|
TotalSum time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalTimeEntry represents the time spent for a project and an activity.
|
||||||
|
type TotalTimeEntry struct {
|
||||||
|
ProjectID string
|
||||||
|
Project string
|
||||||
|
ActivityID string
|
||||||
|
Activity string
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeTotals collects the time spent per project and activity for a time range.
|
||||||
|
func GetTimeTotals(start, end time.Time) (TotalTime, error) {
|
||||||
|
totalTime := TotalTime{
|
||||||
|
Totals: []*TotalTimeEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, entriesErr := readTimeRange(start, end)
|
||||||
|
if entriesErr != nil {
|
||||||
|
return totalTime, entriesErr
|
||||||
|
}
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return totalTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalsMap := make(map[string]map[string]time.Duration)
|
||||||
|
for _, entry := range entries {
|
||||||
|
project, projectFound := totalsMap[entry.ProjectID]
|
||||||
|
if !projectFound {
|
||||||
|
project = make(map[string]time.Duration)
|
||||||
|
totalsMap[entry.ProjectID] = project
|
||||||
|
}
|
||||||
|
|
||||||
|
activity := project[entry.ActivityID]
|
||||||
|
project[entry.ActivityID] = activity + entry.End.Sub(entry.Start)
|
||||||
|
}
|
||||||
|
if len(totalsMap) == 0 {
|
||||||
|
return totalTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for projectID, activities := range totalsMap {
|
||||||
|
for activity, duration := range activities {
|
||||||
|
totalEntry := TotalTimeEntry{
|
||||||
|
ProjectID: projectID,
|
||||||
|
ActivityID: activity,
|
||||||
|
Duration: duration,
|
||||||
|
}
|
||||||
|
totalTime.Totals = append(totalTime.Totals, &totalEntry)
|
||||||
|
totalTime.TotalSum = totalTime.TotalSum + duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeSummary gathers time entries for a given day. The entries are sorted by start time ascending.
|
||||||
|
func GetTimeSummary() ([]*Time, error) {
|
||||||
|
dayStart := time.Now().Truncate(24 * time.Hour)
|
||||||
|
dayEnd := time.Now().Truncate(24*time.Hour).AddDate(0, 0, 1).Add(-1 * time.Second)
|
||||||
|
entries, entriesErr := readTimeRange(dayStart, dayEnd)
|
||||||
|
if entriesErr != nil {
|
||||||
|
return nil, entriesErr
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Start.Before(entries[j].Start)
|
||||||
|
})
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
117
lib/timer.go
Normal file
117
lib/timer.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoTimerActiveError struct{}
|
||||||
|
|
||||||
|
func (e *NoTimerActiveError) Error() string {
|
||||||
|
return "No timer is currently active."
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTimer starts a new timer.
|
||||||
|
func StartTimer() (*Time, error) {
|
||||||
|
// Stop active timer
|
||||||
|
activeTimer, _ := GetTimer()
|
||||||
|
if activeTimer != nil {
|
||||||
|
timer, stopErr := StopTimer()
|
||||||
|
if stopErr != nil {
|
||||||
|
return timer, stopErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new timer
|
||||||
|
newTimer := Time{
|
||||||
|
Start: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
setErr := setTimer(&newTimer)
|
||||||
|
if setErr != nil {
|
||||||
|
return nil, setErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newTimer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTimer sets the project and activity on the current timer.
|
||||||
|
func UpdateTimer(project *Project, activity *Activity) (*Time, error) {
|
||||||
|
timer, timerErr := GetTimer()
|
||||||
|
if timerErr != nil {
|
||||||
|
return timer, timerErr
|
||||||
|
}
|
||||||
|
if timer == nil {
|
||||||
|
return nil, &NoTimerActiveError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.ProjectID = project.ID
|
||||||
|
timer.ActivityID = activity.ID
|
||||||
|
|
||||||
|
setError := setTimer(timer)
|
||||||
|
if setError != nil {
|
||||||
|
return timer, setError
|
||||||
|
}
|
||||||
|
|
||||||
|
return timer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopTimer stops the current timer and records the resulting time entry.
|
||||||
|
func StopTimer() (*Time, error) {
|
||||||
|
timer, timerErr := GetTimer()
|
||||||
|
if timerErr != nil {
|
||||||
|
return timer, timerErr
|
||||||
|
}
|
||||||
|
if timer == nil {
|
||||||
|
return nil, &NoTimerActiveError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.End = time.Now()
|
||||||
|
recordErr := SetTime(timer)
|
||||||
|
if recordErr != nil {
|
||||||
|
return timer, recordErr
|
||||||
|
}
|
||||||
|
|
||||||
|
file, fileErr := getTimerFile()
|
||||||
|
if fileErr != nil {
|
||||||
|
return timer, fileErr
|
||||||
|
}
|
||||||
|
|
||||||
|
removeErr := os.Remove(file)
|
||||||
|
if removeErr != nil {
|
||||||
|
return timer, removeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimer returns the active timer.
|
||||||
|
func GetTimer() (*Time, error) {
|
||||||
|
file, fileErr := getTimerFile()
|
||||||
|
if fileErr != nil {
|
||||||
|
return nil, fileErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return readTime(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTimer persists a time entry as current timer. If there is already a current timer, it will be overwritten.
|
||||||
|
func setTimer(entry *Time) error {
|
||||||
|
file, fileErr := getTimerFile()
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeTime(entry, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTimerFile returns the file name for the timer.
|
||||||
|
func getTimerFile() (string, error) {
|
||||||
|
folder, err := getDataDir("time")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(folder, "timer.json"), nil
|
||||||
|
}
|
||||||
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