package manifest import ( "encoding/json" "os" "path/filepath" "strings" "sync" ) type jsonlManifest struct { mu sync.Mutex entries map[string]Entry path string file *os.File syncFunc func() error } var jsonlSaveHook func() error func SetJSONLSaveHook(fn func() error) func() error { old := jsonlSaveHook jsonlSaveHook = fn return old } func JSONLPath(dir string) string { return filepath.Join(dir, "downloads.jsonl") } func LoadJSONL(dir string) *jsonlManifest { m := &jsonlManifest{ entries: make(map[string]Entry), path: JSONLPath(dir), } data, err := os.ReadFile(m.path) if err != nil { return m } type entryWithID struct { ID string `json:"id"` Filename string `json:"filename"` Size int64 `json:"size"` Cloud string `json:"cloud"` 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 != "" { m.entries[raw.ID] = Entry{ Filename: raw.Filename, Size: raw.Size, Cloud: raw.Cloud, Exported: raw.Exported, } } } return m } func (m *jsonlManifest) Has(id string) bool { m.mu.Lock() defer m.mu.Unlock() _, ok := m.entries[id] return ok } func (m *jsonlManifest) Add(id string, filename string, size int64, cloud string) { m.mu.Lock() defer m.mu.Unlock() entry := newEntry(id, filename, size, cloud) m.entries[id] = entry if m.file != nil { data, _ := json.Marshal(struct { ID string `json:"id"` Filename string `json:"filename"` Size int64 `json:"size"` Cloud string `json:"cloud"` Exported int64 `json:"exported"` }{ID: id, Filename: entry.Filename, Size: entry.Size, Cloud: entry.Cloud, Exported: entry.Exported}) m.file.Write(data) m.file.Write([]byte("\n")) } } func (m *jsonlManifest) Save() error { m.mu.Lock() defer m.mu.Unlock() if m.syncFunc != nil { return m.syncFunc() } if jsonlSaveHook != nil { return jsonlSaveHook() } if m.file != nil { return m.file.Sync() } return nil } func (m *jsonlManifest) Close() { m.mu.Lock() defer m.mu.Unlock() if m.file != nil { m.file.Close() m.file = nil } } func (m *jsonlManifest) 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 *jsonlManifest) Entries() map[string]Entry { m.mu.Lock() defer m.mu.Unlock() out := make(map[string]Entry, len(m.entries)) for k, v := range m.entries { out[k] = v } return out }