package jsonl import ( "encoding/json" "os" "path/filepath" "strings" "sync" "gitea.k3s.k0.nu/tools/photocli/internal/manifest/types" ) type Store struct { mu sync.Mutex entries map[string]types.Entry path string file *os.File SyncFunc func() error } var saveHook func() error func SetSaveHook(fn func() error) func() error { old := saveHook saveHook = fn return old } func Path(dir string) string { return filepath.Join(dir, "downloads.jsonl") } func Load(dir string) *Store { m := &Store{ entries: make(map[string]types.Entry), path: Path(dir), } data, err := os.ReadFile(m.path) if err != nil { return m } type entryWithID struct { ID string `json:"id"` Filename string `json:"filename"` Path string `json:"path,omitempty"` Size int64 `json:"size"` Cloud string `json:"cloud"` Checksum string `json:"checksum,omitempty"` Exported int64 `json:"exported"` } for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line == "" { continue } var raw entryWithID if json.Unmarshal([]byte(line), &raw) == nil && raw.ID != "" { if raw.Path == "" { raw.Path = raw.Filename } m.entries[raw.ID] = types.Entry{ ID: raw.ID, Filename: raw.Filename, Path: raw.Path, Size: raw.Size, Cloud: raw.Cloud, Checksum: raw.Checksum, Exported: raw.Exported, } } } return m } func (m *Store) Has(id string) bool { m.mu.Lock() defer m.mu.Unlock() _, ok := m.entries[id] return ok } func (m *Store) ManifestFormat() types.Format { return types.FormatJSONL } func (m *Store) Add(id string, filename string, size int64, cloud string) { m.AddEntry(types.NewEntry(id, filename, filename, size, cloud)) } func (m *Store) AddEntry(entry types.Entry) { m.mu.Lock() defer m.mu.Unlock() if entry.Path == "" { entry.Path = entry.Filename } m.entries[entry.ID] = entry if m.file != nil { data, _ := json.Marshal(struct { ID string `json:"id"` Filename string `json:"filename"` Path string `json:"path,omitempty"` Size int64 `json:"size"` Cloud string `json:"cloud"` Checksum string `json:"checksum,omitempty"` Exported int64 `json:"exported"` }{ID: entry.ID, Filename: entry.Filename, Path: entry.Path, Size: entry.Size, Cloud: entry.Cloud, Checksum: entry.Checksum, Exported: entry.Exported}) m.file.Write(data) m.file.Write([]byte("\n")) } } func (m *Store) Save() error { m.mu.Lock() defer m.mu.Unlock() if m.SyncFunc != nil { return m.SyncFunc() } if saveHook != nil { return saveHook() } if m.file != nil { return m.file.Sync() } return nil } func (m *Store) Close() { m.mu.Lock() defer m.mu.Unlock() if m.file != nil { m.file.Close() m.file = nil } } func (m *Store) OpenAppend() error { m.mu.Lock() defer m.mu.Unlock() if m.file != nil { return nil } if err := os.MkdirAll(filepath.Dir(m.path), 0755); err != nil { return err } f, err := os.OpenFile(m.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return err } m.file = f return nil } func (m *Store) Entries() map[string]types.Entry { m.mu.Lock() defer m.mu.Unlock() out := make(map[string]types.Entry, len(m.entries)) for k, v := range m.entries { out[k] = v } return out } func FileExists(path string) bool { info, err := os.Stat(path) if err != nil { return false } return !info.IsDir() }