1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
package worktime
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
const dbFilePattern = "db.*.json"
// Entry is a single worktime event in a host database.
type Entry struct {
Action string `json:"action"`
What string `json:"what"`
Epoch int64 `json:"epoch"`
Source string `json:"source"`
Human string `json:"human"`
Value int64 `json:"value,omitempty"`
Descr string `json:"descr,omitempty"`
}
// Database is the on-disk JSON structure used by worktime.
type Database struct {
Entries map[string][]Entry `json:"entries"`
}
// LoadAll reads all db.*.json files from dbDir, merges entries, and sorts by epoch.
func LoadAll(dbDir string) ([]Entry, error) {
if strings.TrimSpace(dbDir) == "" {
return nil, errors.New("db directory must not be empty")
}
dbFiles, err := filepath.Glob(filepath.Join(dbDir, dbFilePattern))
if err != nil {
return nil, fmt.Errorf("glob databases in %q: %w", dbDir, err)
}
entries := make([]Entry, 0)
for _, dbFile := range dbFiles {
db, err := loadDatabaseFile(dbFile)
if err != nil {
return nil, err
}
for _, hostEntries := range db.Entries {
entries = append(entries, hostEntries...)
}
}
sortEntries(entries)
return entries, nil
}
// LoadHost reads one host database from dbDir. Missing files return an empty host section.
func LoadHost(dbDir, hostname string) (Database, error) {
host, err := normalizeHostname(hostname)
if err != nil {
return Database{}, err
}
if strings.TrimSpace(dbDir) == "" {
return Database{}, errors.New("db directory must not be empty")
}
dbFile := filepath.Join(dbDir, dbFileName(host))
db, err := loadDatabaseFile(dbFile)
if err != nil {
if os.IsNotExist(err) {
return newHostDatabase(host), nil
}
return Database{}, err
}
if _, ok := db.Entries[host]; !ok {
db.Entries[host] = []Entry{}
}
sortEntries(db.Entries[host])
return db, nil
}
// SaveHost writes one host database to dbDir as db.<hostname>.json.
func SaveHost(dbDir, hostname string, db Database) error {
host, err := normalizeHostname(hostname)
if err != nil {
return err
}
if strings.TrimSpace(dbDir) == "" {
return errors.New("db directory must not be empty")
}
if db.Entries == nil {
db.Entries = map[string][]Entry{}
}
if _, ok := db.Entries[host]; !ok {
db.Entries[host] = []Entry{}
}
sortEntries(db.Entries[host])
data, err := json.MarshalIndent(db, "", " ")
if err != nil {
return fmt.Errorf("encode database for host %q: %w", host, err)
}
data = append(data, '\n')
if err := os.MkdirAll(dbDir, 0o755); err != nil {
return fmt.Errorf("create db directory %q: %w", dbDir, err)
}
dbFile := filepath.Join(dbDir, dbFileName(host))
if err := os.WriteFile(dbFile, data, 0o644); err != nil {
return fmt.Errorf("write db file %q: %w", dbFile, err)
}
return nil
}
func loadDatabaseFile(dbFile string) (Database, error) {
var db Database
data, err := os.ReadFile(dbFile)
if err != nil {
return db, err
}
if err := json.Unmarshal(data, &db); err != nil {
return db, fmt.Errorf("parse db file %q: %w", dbFile, err)
}
if db.Entries == nil {
db.Entries = map[string][]Entry{}
}
return db, nil
}
func sortEntries(entries []Entry) {
sort.SliceStable(entries, func(i, j int) bool {
if entries[i].Epoch != entries[j].Epoch {
return entries[i].Epoch < entries[j].Epoch
}
if entries[i].Source != entries[j].Source {
return entries[i].Source < entries[j].Source
}
return entries[i].Action < entries[j].Action
})
}
func normalizeHostname(hostname string) (string, error) {
host := strings.TrimSpace(hostname)
if host == "" {
return "", errors.New("hostname must not be empty")
}
return host, nil
}
func dbFileName(hostname string) string {
return "db." + hostname + ".json"
}
func newHostDatabase(hostname string) Database {
return Database{
Entries: map[string][]Entry{
hostname: {},
},
}
}
|