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
+18 -24
View File
@@ -9,7 +9,6 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
assets "shared-album"
"shared-album/internal/albums"
@@ -23,16 +22,17 @@ import (
"shared-album/internal/social"
"shared-album/internal/sse"
"shared-album/internal/storage"
"shared-album/internal/store"
)
type Server struct {
cfg *config.Config
pool *pgxpool.Pool
repos store.Repos
ping func(ctx context.Context) error
storage storage.Backend
hub *sse.Hub
albumsStore *albums.Store
albums *albums.Service
albums *albums.Service
auth *auth.Service
@@ -48,36 +48,30 @@ type Server struct {
share *sharelinks.Service
}
func New(cfg *config.Config, pool *pgxpool.Pool, st storage.Backend, hub *sse.Hub) *Server {
authStore := auth.NewStore(pool)
// New wires the domain services over the selected metadata repositories. ping
// reports the health of the metadata backend so health stays backend-agnostic.
func New(cfg *config.Config, repos store.Repos, ping func(ctx context.Context) error, st storage.Backend, hub *sse.Hub) *Server {
tm := auth.NewTokenManager(cfg.Auth.JWTSecret, cfg.Auth.AccessTTL, cfg.Auth.RefreshTTL)
authSvc := auth.NewService(authStore, tm)
authSvc := auth.NewService(repos.Auth, tm)
albumsStore := albums.NewStore(pool)
membersStore := members.NewStore(pool)
notifSvc := notifications.NewService(repos.Notifications, hub)
notifStore := notifications.NewStore(pool)
notifSvc := notifications.NewService(notifStore, hub)
albumsSvc := albums.NewService(repos.Albums, repos.Members, hub, st)
membersSvc := members.NewService(repos.Members, repos.Albums, hub, notifSvc)
albumsSvc := albums.NewService(albumsStore, membersStore, hub, st)
membersSvc := members.NewService(membersStore, albumsStore, hub, notifSvc)
photosStore := photos.NewStore(pool)
photosSvc := photos.NewService(photosStore, albumsSvc, st, hub)
photosSvc := photos.NewService(repos.Photos, albumsSvc, st, hub)
photosWorker := photos.NewWorker(photosSvc, cfg.Worker.Interval, cfg.Worker.Batch, cfg.Worker.PendingTTL)
socialStore := social.NewStore(pool)
socialSvc := social.NewService(socialStore, albumsSvc, photosStore, hub, notifSvc)
socialSvc := social.NewService(repos.Social, albumsSvc, repos.Photos, hub, notifSvc)
shareStore := sharelinks.NewStore(pool)
shareSvc := sharelinks.NewService(shareStore, albumsSvc)
shareSvc := sharelinks.NewService(repos.ShareLinks, albumsSvc)
return &Server{
cfg: cfg,
pool: pool,
repos: repos,
ping: ping,
storage: st,
hub: hub,
albumsStore: albumsStore,
albums: albumsSvc,
auth: authSvc,
membersSvc: membersSvc,
@@ -183,7 +177,7 @@ func (s *Server) Router() *gin.Engine {
func (s *Server) health(c *gin.Context) {
ctx := c.Request.Context()
dbStatus := "ok"
if err := s.pool.Ping(ctx); err != nil {
if err := s.ping(ctx); err != nil {
dbStatus = "error"
}
storageLabel := s.cfg.Storage.Backend
@@ -243,7 +237,7 @@ func (s *Server) shareAlbum(c *gin.Context) {
httpx.RespondError(c, err)
return
}
album, err := s.albumsStore.GetByID(c.Request.Context(), link.AlbumID)
album, err := s.repos.Albums.GetByID(c.Request.Context(), link.AlbumID)
if err != nil {
httpx.RespondError(c, httpx.NotFound("album_not_found", "album not found"))
return
+2 -12
View File
@@ -9,7 +9,6 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
"shared-album/internal/config"
"shared-album/internal/storage"
@@ -24,23 +23,14 @@ func mustSigner(t *testing.T) *storage.Signer {
return signer
}
func lazyPool(t *testing.T) *pgxpool.Pool {
t.Helper()
pool, err := pgxpool.New(context.Background(), "postgres://u:p@127.0.0.1:1/x?sslmode=disable")
if err != nil {
t.Fatalf("pool: %v", err)
}
t.Cleanup(pool.Close)
return pool
}
func TestHealthStorageLabel(t *testing.T) {
gin.SetMode(gin.TestMode)
fs, err := storage.NewFS(t.TempDir(), mustSigner(t))
if err != nil {
t.Fatalf("fs: %v", err)
}
srv := &Server{cfg: &config.Config{}, pool: lazyPool(t), storage: fs}
ok := func(context.Context) error { return nil }
srv := &Server{cfg: &config.Config{}, ping: ok, storage: fs}
call := func() map[string]any {
rec := httptest.NewRecorder()
+3 -1
View File
@@ -15,6 +15,7 @@ import (
"shared-album/internal/config"
"shared-album/internal/sse"
"shared-album/internal/storage"
"shared-album/internal/store"
)
func newTestServerWithFS(t *testing.T) (*storage.FS, *gin.Engine) {
@@ -28,7 +29,8 @@ func newTestServerWithFS(t *testing.T) (*storage.FS, *gin.Engine) {
if err != nil {
t.Fatalf("new fs: %v", err)
}
srv := New(&config.Config{}, nil, fs, sse.NewHub(1))
noop := func(context.Context) error { return nil }
srv := New(&config.Config{}, store.Repos{}, noop, fs, sse.NewHub(1))
return fs, srv.Router()
}
+2 -1
View File
@@ -27,6 +27,7 @@ import (
"shared-album/internal/sharelinks"
"shared-album/internal/sse"
"shared-album/internal/storage"
"shared-album/internal/store"
)
func integrationPool(t *testing.T) *pgxpool.Pool {
@@ -70,7 +71,7 @@ func runShareFlow(t *testing.T, backend storage.Backend, verify func(t *testing.
gin.SetMode(gin.TestMode)
hub := sse.NewHub(1)
srv := New(&config.Config{}, pool, backend, hub)
srv := New(&config.Config{}, store.NewPostgres(pool), pool.Ping, backend, hub)
router := srv.Router()
authStore := auth.NewStore(pool)