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.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user