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:
iu
2026-10-11 10:08:31 +03:00
parent ec8d907565
commit 176a8d8ced
69 changed files with 4174 additions and 95 deletions
+15
View File
@@ -0,0 +1,15 @@
package sharelinks
import (
"context"
"shared-album/internal/models"
)
// Repository is the persistence contract for the share-links domain.
type Repository interface {
Create(ctx context.Context, l *models.ShareLink) error
ListByAlbum(ctx context.Context, albumID string) ([]models.ShareLink, error)
GetByToken(ctx context.Context, token string) (*models.ShareLink, error)
Revoke(ctx context.Context, id, albumID string) error
}
+2 -2
View File
@@ -16,11 +16,11 @@ type AlbumAccess interface {
}
type Service struct {
store *Store
store Repository
albums AlbumAccess
}
func NewService(store *Store, albums AlbumAccess) *Service {
func NewService(store Repository, albums AlbumAccess) *Service {
return &Service{store: store, albums: albums}
}
@@ -0,0 +1,90 @@
package sharelinks
import (
"context"
"database/sql"
"errors"
"time"
"shared-album/internal/models"
"shared-album/internal/sqliteutil"
)
// SQLiteStore is the SQLite implementation of Repository.
type SQLiteStore struct {
db *sql.DB
}
func NewSQLiteStore(db *sql.DB) *SQLiteStore { return &SQLiteStore{db: db} }
func (s *SQLiteStore) Create(ctx context.Context, l *models.ShareLink) error {
var createdAt string
err := s.db.QueryRowContext(ctx,
`INSERT INTO share_links (id, album_id, token, created_by, expires_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING created_at`,
l.ID, l.AlbumID, l.Token, l.CreatedBy, sqliteutil.FormatTime(l.ExpiresAt),
).Scan(&createdAt)
if err != nil {
return err
}
l.CreatedAt = sqliteutil.ParseTime(createdAt)
return nil
}
func (s *SQLiteStore) ListByAlbum(ctx context.Context, albumID string) ([]models.ShareLink, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT id, album_id, token, created_by, created_at, expires_at, revoked_at
FROM share_links WHERE album_id = $1 ORDER BY created_at DESC`, albumID)
if err != nil {
return nil, err
}
defer rows.Close()
out := []models.ShareLink{}
for rows.Next() {
var l models.ShareLink
var createdAt, expiresAt string
var revokedAt sql.NullString
if err := rows.Scan(&l.ID, &l.AlbumID, &l.Token, &l.CreatedBy, &createdAt, &expiresAt, &revokedAt); err != nil {
return nil, err
}
l.CreatedAt = sqliteutil.ParseTime(createdAt)
l.ExpiresAt = sqliteutil.ParseTime(expiresAt)
l.RevokedAt = sqliteutil.ParseNullTime(revokedAt)
out = append(out, l)
}
return out, rows.Err()
}
func (s *SQLiteStore) GetByToken(ctx context.Context, token string) (*models.ShareLink, error) {
var l models.ShareLink
var createdAt, expiresAt string
var revokedAt sql.NullString
err := s.db.QueryRowContext(ctx,
`SELECT id, album_id, token, created_by, created_at, expires_at, revoked_at
FROM share_links WHERE token = $1`, token).
Scan(&l.ID, &l.AlbumID, &l.Token, &l.CreatedBy, &createdAt, &expiresAt, &revokedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
l.CreatedAt = sqliteutil.ParseTime(createdAt)
l.ExpiresAt = sqliteutil.ParseTime(expiresAt)
l.RevokedAt = sqliteutil.ParseNullTime(revokedAt)
return &l, nil
}
func (s *SQLiteStore) Revoke(ctx context.Context, id, albumID string) error {
res, err := s.db.ExecContext(ctx,
`UPDATE share_links SET revoked_at = $3 WHERE id = $1 AND album_id = $2 AND revoked_at IS NULL`,
id, albumID, sqliteutil.FormatTime(time.Now()))
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return ErrNotFound
}
return nil
}