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,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()
|
||||
}
|
||||
Reference in New Issue
Block a user