feat(metadata): SQLite backend, storage reconciliation and local defaults
- pluggable per-domain Repository interfaces; PostgreSQL and SQLite impls - SQLite on modernc.org/sqlite (WAL, busy_timeout, foreign_keys, immediate tx) - separate migrations/sqlite and db.MigrateSQLite; store.NewPostgres/NewSQLite - cmd/reconcile compares object storage with the journal (report, then --delete) - storage.Backend.List and photos.Repository.AllStorageKeys - DB_BACKEND/SQLITE_PATH and STORAGE_BACKEND/FS_ROOT selectable; local defaults (sqlite ./galo.db, fs ./data) - Makefile reconcile/reconcile-delete; docs and .env.example updated
This commit is contained in:
@@ -59,10 +59,16 @@ type Storage struct {
|
||||
URLTTL time.Duration
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Backend string
|
||||
SQLitePath string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Postgres Postgres
|
||||
S3 S3
|
||||
Storage Storage
|
||||
Database Database
|
||||
Auth Auth
|
||||
Server Server
|
||||
SSE SSE
|
||||
@@ -138,11 +144,15 @@ func Load() (*Config, error) {
|
||||
UsePathStyle: pathStyle,
|
||||
},
|
||||
Storage: Storage{
|
||||
Backend: getEnv("STORAGE_BACKEND", "s3"),
|
||||
FSRoot: getEnv("FS_ROOT", ""),
|
||||
Backend: getEnv("STORAGE_BACKEND", "fs"),
|
||||
FSRoot: getEnv("FS_ROOT", "./data"),
|
||||
URLSecret: getEnv("URL_SECRET", ""),
|
||||
URLTTL: urlTTL,
|
||||
},
|
||||
Database: Database{
|
||||
Backend: getEnv("DB_BACKEND", "sqlite"),
|
||||
SQLitePath: getEnv("SQLITE_PATH", "./galo.db"),
|
||||
},
|
||||
Auth: Auth{
|
||||
JWTSecret: getEnv("JWT_SECRET", ""),
|
||||
AccessTTL: accessTTL,
|
||||
@@ -166,8 +176,18 @@ func Load() (*Config, error) {
|
||||
if cfg.Auth.JWTSecret == "" {
|
||||
return nil, fmt.Errorf("JWT_SECRET is required")
|
||||
}
|
||||
if cfg.Postgres.Password == "" {
|
||||
return nil, fmt.Errorf("PG_PASSWORD is required")
|
||||
|
||||
switch cfg.Database.Backend {
|
||||
case "postgres":
|
||||
if cfg.Postgres.Password == "" {
|
||||
return nil, fmt.Errorf("PG_PASSWORD is required")
|
||||
}
|
||||
case "sqlite":
|
||||
if cfg.Database.SQLitePath == "" {
|
||||
return nil, fmt.Errorf("SQLITE_PATH is required when DB_BACKEND=sqlite")
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid DB_BACKEND %q (want postgres or sqlite)", cfg.Database.Backend)
|
||||
}
|
||||
|
||||
switch cfg.Storage.Backend {
|
||||
|
||||
@@ -12,15 +12,15 @@ func setRequiredEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadStorageBackend(t *testing.T) {
|
||||
t.Run("defaults to s3", func(t *testing.T) {
|
||||
t.Run("defaults to fs", func(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("STORAGE_BACKEND", "")
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.Storage.Backend != "s3" {
|
||||
t.Fatalf("backend = %q, want s3", cfg.Storage.Backend)
|
||||
if cfg.Storage.Backend != "fs" || cfg.Storage.FSRoot != "./data" {
|
||||
t.Fatalf("storage = %+v", cfg.Storage)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,13 +37,17 @@ func TestLoadStorageBackend(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fs without root", func(t *testing.T) {
|
||||
t.Run("fs empty root falls back to default", func(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("STORAGE_BACKEND", "fs")
|
||||
t.Setenv("FS_ROOT", "")
|
||||
t.Setenv("URL_SECRET", "")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected error for fs without FS_ROOT")
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.Storage.FSRoot != "./data" {
|
||||
t.Fatalf("FSRoot = %q, want ./data", cfg.Storage.FSRoot)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -68,3 +72,63 @@ func TestLoadStorageBackend(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadDatabaseBackend(t *testing.T) {
|
||||
t.Run("defaults to sqlite", func(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("DB_BACKEND", "")
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.Database.Backend != "sqlite" || cfg.Database.SQLitePath != "./galo.db" {
|
||||
t.Fatalf("database = %+v", cfg.Database)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("postgres requires password", func(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", "s")
|
||||
t.Setenv("PG_PASSWORD", "")
|
||||
t.Setenv("DB_BACKEND", "postgres")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected error for postgres without PG_PASSWORD")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sqlite with path", func(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", "s")
|
||||
t.Setenv("PG_PASSWORD", "")
|
||||
t.Setenv("DB_BACKEND", "sqlite")
|
||||
t.Setenv("SQLITE_PATH", "/data/galo.db")
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.Database.Backend != "sqlite" || cfg.Database.SQLitePath != "/data/galo.db" {
|
||||
t.Fatalf("database = %+v", cfg.Database)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sqlite empty path falls back to default", func(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", "s")
|
||||
t.Setenv("PG_PASSWORD", "")
|
||||
t.Setenv("DB_BACKEND", "sqlite")
|
||||
t.Setenv("SQLITE_PATH", "")
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if cfg.Database.SQLitePath != "./galo.db" {
|
||||
t.Fatalf("SQLitePath = %q, want ./galo.db", cfg.Database.SQLitePath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid backend", func(t *testing.T) {
|
||||
t.Setenv("JWT_SECRET", "s")
|
||||
t.Setenv("PG_PASSWORD", "p")
|
||||
t.Setenv("DB_BACKEND", "mysql")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected error for invalid backend")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user