v0.10.0: ports and adapters refactor
pipeline / test (push) Has been cancelled
pipeline / build (push) Has been cancelled

- 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:
Ein Anderssono
2026-06-15 08:27:38 +02:00
parent 9cd048d9f3
commit c9ac014473
28 changed files with 2061 additions and 927 deletions
+165
View File
@@ -0,0 +1,165 @@
package sqlite
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"gitea.k3s.k0.nu/tools/photocli/internal/manifest/types"
)
type OpenerFunc func(driverName, dataSourceName string) (*sql.DB, error)
var openerOverride OpenerFunc
func SetOpener(fn OpenerFunc) (old OpenerFunc) {
old = openerOverride
openerOverride = fn
return old
}
type Store struct {
path string
db *sql.DB
open OpenerFunc
execFunc func(query string, args ...any) (sql.Result, error)
}
func Path(dir string) string {
return filepath.Join(dir, "downloads.db")
}
func Load(dir string) (*Store, error) {
m := &Store{
path: Path(dir),
open: opener(),
}
return m, nil
}
func opener() OpenerFunc {
if openerOverride != nil {
return openerOverride
}
return sql.Open
}
func (m *Store) OpenAppend() error {
if m.db != nil {
return nil
}
if err := os.MkdirAll(filepath.Dir(m.path), 0755); err != nil {
return err
}
opener := m.open
if opener == nil {
opener = sql.Open
}
db, err := opener("sqlite", m.path)
if err != nil {
return fmt.Errorf("open sqlite: %w", err)
}
execFn := m.execFunc
if execFn == nil {
execFn = db.Exec
}
_, err = execFn(`CREATE TABLE IF NOT EXISTS downloads (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL DEFAULT '',
path TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
cloud TEXT NOT NULL DEFAULT '',
checksum TEXT NOT NULL DEFAULT '',
exported INTEGER NOT NULL DEFAULT 0
)`)
if err != nil {
db.Close()
return fmt.Errorf("create table: %w", err)
}
_, _ = execFn(`ALTER TABLE downloads ADD COLUMN path TEXT NOT NULL DEFAULT ''`)
_, _ = execFn(`ALTER TABLE downloads ADD COLUMN checksum TEXT NOT NULL DEFAULT ''`)
_, err = execFn(`CREATE INDEX IF NOT EXISTS idx_downloads_id ON downloads(id)`)
if err != nil {
db.Close()
return fmt.Errorf("create index: %w", err)
}
m.db = db
return nil
}
func (m *Store) DB() *sql.DB { return m.db }
func (m *Store) SetOpen(fn OpenerFunc) { m.open = fn }
func (m *Store) SetExecFunc(fn func(query string, args ...any) (sql.Result, error)) {
m.execFunc = fn
}
func (m *Store) Has(id string) bool {
if m.db == nil {
return false
}
var count int
err := m.db.QueryRow(`SELECT COUNT(*) FROM downloads WHERE id = ?`, id).Scan(&count)
if err != nil {
return false
}
return count > 0
}
func (m *Store) ManifestFormat() types.Format {
return types.FormatSQLite
}
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) {
if m.db == nil {
return
}
if entry.Path == "" {
entry.Path = entry.Filename
}
m.db.Exec(`INSERT OR REPLACE INTO downloads (id, filename, path, size, cloud, checksum, exported) VALUES (?, ?, ?, ?, ?, ?, ?)`,
entry.ID, entry.Filename, entry.Path, entry.Size, entry.Cloud, entry.Checksum, entry.Exported)
}
func (m *Store) Save() error { return nil }
func (m *Store) Close() {
if m.db != nil {
m.db.Close()
m.db = nil
}
}
func (m *Store) Entries() map[string]types.Entry {
if m.db == nil {
return nil
}
out := make(map[string]types.Entry)
rows, err := m.db.Query(`SELECT id, filename, path, size, cloud, checksum, exported FROM downloads`)
if err != nil {
return out
}
defer rows.Close()
for rows.Next() {
var e types.Entry
if err := rows.Scan(&e.ID, &e.Filename, &e.Path, &e.Size, &e.Cloud, &e.Checksum, &e.Exported); err == nil {
if e.Path == "" {
e.Path = e.Filename
}
out[e.ID] = e
}
}
return out
}
func FileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}