Files
photocli/internal/manifest/jsonl/store.go
T
Ein Anderssono c9ac014473
pipeline / test (push) Has been cancelled
pipeline / build (push) Has been cancelled
v0.10.0: ports and adapters refactor
- Extract shared manifest types into internal/manifest/types leaf package.
- Extract SQLite adapter into internal/manifest/sqlite.
- Extract JSONL adapter into internal/manifest/jsonl.
- Isolate modernc.org/sqlite import to sqlite/adapter.go.
- Add adapter-backed registry with manifest.Default.
- Adapter-agnostic ConvertManifest in types/.
- MemoryAdapter for in-memory manifest testing.
- CLI uses manifest.Default registry directly.
- SQLite LogWriter type assertion moved into SQLiteAdapter.
- Manifest interface includes Entries(); EntryReader removed.
- No behavior changes. 100% coverage across all 6 packages.
2026-06-15 08:27:38 +02:00

170 lines
3.4 KiB
Go

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()
}