package store import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "sync" "git.casaderoll.de/michael/urbm/internal/model" ) type Store struct { dir string mu sync.RWMutex } func New(dir string) *Store { return &Store{dir: dir} } func (s *Store) Init() error { if err := os.MkdirAll(s.dir, 0700); err != nil { return err } if _, err := os.Stat(s.configPath()); errors.Is(err, os.ErrNotExist) { return s.SaveConfig(model.DefaultConfig()) } return nil } func (s *Store) LoadConfig() (model.Config, error) { s.mu.RLock() defer s.mu.RUnlock() var c model.Config if err := readJSON(s.configPath(), &c); err != nil { return c, err } c = model.NormalizeConfig(c) return c, model.ValidateConfig(c) } func (s *Store) SaveConfig(c model.Config) error { c = model.NormalizeConfig(c) if err := model.ValidateConfig(c); err != nil { return err } s.mu.Lock() defer s.mu.Unlock() return writeJSONAtomic(s.configPath(), c, 0600) } func (s *Store) LoadRuns() ([]model.Run, error) { s.mu.RLock() defer s.mu.RUnlock() var runs []model.Run if err := readJSON(s.runsPath(), &runs); errors.Is(err, os.ErrNotExist) { return []model.Run{}, nil } else if err != nil { return nil, err } return runs, nil } func (s *Store) SaveRuns(runs []model.Run) error { s.mu.Lock() defer s.mu.Unlock() if len(runs) > 500 { runs = runs[len(runs)-500:] } return writeJSONAtomic(s.runsPath(), runs, 0600) } func (s *Store) configPath() string { return filepath.Join(s.dir, "config.json") } func (s *Store) runsPath() string { return filepath.Join(s.dir, "runs.json") } func readJSON(path string, target any) error { b, err := os.ReadFile(path) if err != nil { return err } if err := json.Unmarshal(b, target); err != nil { return fmt.Errorf("decode %s: %w", path, err) } return nil } func writeJSONAtomic(path string, value any, mode os.FileMode) error { b, err := json.MarshalIndent(value, "", " ") if err != nil { return err } b = append(b, '\n') tmp, err := os.CreateTemp(filepath.Dir(path), ".urbm-*") if err != nil { return err } tmpName := tmp.Name() defer os.Remove(tmpName) if err := tmp.Chmod(mode); err != nil { tmp.Close() return err } if _, err := tmp.Write(b); err != nil { tmp.Close() return err } if err := tmp.Sync(); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmpName, path) }