Files
ds-galo/backend/internal/albums/service.go
iu 176a8d8ced 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
2026-10-11 10:08:31 +03:00

162 lines
4.0 KiB
Go

package albums
import (
"context"
"errors"
"strings"
"shared-album/internal/auth"
"shared-album/internal/httpx"
"shared-album/internal/models"
"shared-album/internal/roles"
"shared-album/internal/sse"
"shared-album/internal/storage"
)
type MemberLookup interface {
Role(ctx context.Context, albumID, userID string) (string, error)
}
type Service struct {
store Repository
members MemberLookup
hub *sse.Hub
storage storage.Backend
}
func NewService(store Repository, members MemberLookup, hub *sse.Hub, storage storage.Backend) *Service {
return &Service{store: store, members: members, hub: hub, storage: storage}
}
type CreateInput struct {
Title string
Description string
}
func (s *Service) Create(ctx context.Context, userID string, in CreateInput) (*models.Album, error) {
title := strings.TrimSpace(in.Title)
if title == "" {
return nil, httpx.BadRequest("invalid_title", "title is required")
}
a := &models.Album{
ID: auth.NewID(),
OwnerID: userID,
Title: title,
Description: strings.TrimSpace(in.Description),
}
if err := s.store.Create(ctx, a); err != nil {
return nil, err
}
return a, nil
}
func (s *Service) List(ctx context.Context, userID string) ([]models.Album, error) {
out, err := s.store.ListForUser(ctx, userID)
if err != nil {
return nil, err
}
for i := range out {
s.attachCover(ctx, &out[i])
}
return out, nil
}
// attachCover fills CoverThumbURL from the signed thumbnail URL, when present.
func (s *Service) attachCover(ctx context.Context, a *models.Album) {
if s.storage == nil || a == nil || a.CoverThumbKey == "" {
return
}
if url, err := s.storage.URL(ctx, a.CoverThumbKey); err == nil {
a.CoverThumbURL = url
}
}
func (s *Service) Access(ctx context.Context, albumID, userID string) (*models.Album, string, error) {
a, err := s.store.GetByID(ctx, albumID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, "", httpx.NotFound("album_not_found", "album not found")
}
return nil, "", err
}
if a.OwnerID == userID {
return a, roles.Owner, nil
}
role, err := s.members.Role(ctx, albumID, userID)
if err != nil {
return nil, "", err
}
if role == "" {
return nil, "", httpx.NotFound("album_not_found", "album not found")
}
return a, role, nil
}
func (s *Service) Get(ctx context.Context, albumID, userID string) (*models.Album, string, error) {
a, role, err := s.Access(ctx, albumID, userID)
if err != nil {
return nil, "", err
}
s.attachCover(ctx, a)
return a, role, nil
}
type UpdateInput struct {
Title *string
Description *string
CoverPhotoID *string
ClearCover bool
}
func (s *Service) Update(ctx context.Context, albumID, userID string, in UpdateInput) (*models.Album, error) {
a, role, err := s.Access(ctx, albumID, userID)
if err != nil {
return nil, err
}
if !roles.CanEditAlbum(role) {
return nil, httpx.Forbidden("forbidden", "only owner and admin can edit the album")
}
if in.Title != nil {
title := strings.TrimSpace(*in.Title)
if title == "" {
return nil, httpx.BadRequest("invalid_title", "title cannot be empty")
}
a.Title = title
}
if in.Description != nil {
a.Description = strings.TrimSpace(*in.Description)
}
if in.ClearCover {
a.CoverPhotoID = nil
} else if in.CoverPhotoID != nil {
a.CoverPhotoID = in.CoverPhotoID
}
if err := s.store.Update(ctx, a); err != nil {
return nil, err
}
if refreshed, err := s.store.GetByID(ctx, a.ID); err == nil {
a = refreshed
}
s.attachCover(ctx, a)
s.hub.Publish(albumID, "album.updated", a)
return a, nil
}
func (s *Service) Delete(ctx context.Context, albumID, userID string) error {
a, role, err := s.Access(ctx, albumID, userID)
if err != nil {
return err
}
if role != roles.Owner {
return httpx.Forbidden("forbidden", "only the owner can delete the album")
}
if err := s.store.TrashPhotosByAlbum(ctx, a.ID); err != nil {
return err
}
if err := s.store.Delete(ctx, a.ID); err != nil {
return err
}
s.hub.Publish(albumID, "album.deleted", map[string]string{"id": a.ID})
return nil
}