Add photo content-hash dedup and local↔S3 library sync.

Uploads skip per-gallery duplicates, checksums can be backfilled, and STORAGE_BACKEND plus sync tooling make switching storage backends safe.
This commit is contained in:
Michilis
2026-08-05 05:41:12 +00:00
parent dafa3711f8
commit 498d7d8a7d
31 changed files with 2216 additions and 64 deletions
+31 -3
View File
@@ -20,6 +20,11 @@ type Config struct {
// JWT_SECRET during the Better Auth migration.
ViewTokenSecret string
// StorageBackend selects the active backend: "auto" (S3 when it is
// configured, local otherwise), "local" or "s3". Explicit values let both
// backends stay configured — required to run `photo-api sync`, and the
// one-line switch after a sync.
StorageBackend string
StoragePath string
S3Endpoint string
S3Region string
@@ -34,12 +39,26 @@ type Config struct {
HeicConverter string // optional explicit converter command; autodetected when empty
}
// S3Enabled mirrors backend/src/lib/storage.ts: S3 is active when both
// S3_ENDPOINT and S3_BUCKET are set.
func (c Config) S3Enabled() bool {
// S3Configured reports whether the S3 credentials are present at all,
// mirroring backend/src/lib/storage.ts: both S3_ENDPOINT and S3_BUCKET set.
// Kept separate from S3Enabled so a configured-but-inactive S3 backend can
// still be reached by `photo-api sync`.
func (c Config) S3Configured() bool {
return c.S3Endpoint != "" && c.S3Bucket != ""
}
// S3Enabled reports whether S3 is the backend serving requests.
func (c Config) S3Enabled() bool {
switch c.StorageBackend {
case "s3":
return true
case "local":
return false
default: // auto
return c.S3Configured()
}
}
func Load() (Config, error) {
loadDotenv(".env")
@@ -48,6 +67,7 @@ func Load() (Config, error) {
DBType: strings.ToLower(env("DB_TYPE", "sqlite")),
DatabaseURL: env("DATABASE_URL", ""),
ViewTokenSecret: env("PHOTO_VIEW_SECRET", env("JWT_SECRET", "")),
StorageBackend: strings.ToLower(env("STORAGE_BACKEND", "auto")),
StoragePath: env("STORAGE_PATH", "./data/photos"),
S3Endpoint: env("S3_ENDPOINT", ""),
S3Region: env("S3_REGION", "auto"),
@@ -70,6 +90,14 @@ func Load() (Config, error) {
if cfg.ViewTokenSecret == "" {
return cfg, fmt.Errorf("PHOTO_VIEW_SECRET is required (or legacy JWT_SECRET as fallback)")
}
switch cfg.StorageBackend {
case "auto", "local", "s3":
default:
return cfg, fmt.Errorf("STORAGE_BACKEND must be auto, local or s3, got %q", cfg.StorageBackend)
}
if cfg.StorageBackend == "s3" && !cfg.S3Configured() {
return cfg, fmt.Errorf("STORAGE_BACKEND=s3 requires S3_ENDPOINT and S3_BUCKET")
}
return cfg, nil
}