c9ac014473
- 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.
166 lines
3.6 KiB
Go
166 lines
3.6 KiB
Go
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()
|
|
}
|