This commit is contained in:
2026-08-22 02:59:16 +02:00
commit 6a5bb1d699
100 changed files with 17409 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
package galleries
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
)
type Repository struct {
db *sql.DB
}
func NewRepository(db *sql.DB) *Repository {
return &Repository{db: db}
}
var ErrNotFound = errors.New("gallery not found")
func (r *Repository) Create(ctx context.Context, userID uuid.UUID, slug, title, clientName, description string) (GalleryRecord, error) {
id := uuid.New()
_, err := r.db.ExecContext(ctx, `
INSERT INTO galleries (id, user_id, slug, title, client_name, description, status, theme_config, branding_config)
VALUES ($1, $2, $3, $4, $5, $6, 'draft', '{}', '{}')
`, id, userID, slug, title, clientName, description)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unique") {
return GalleryRecord{}, fmt.Errorf("gallery slug already exists: %w", err)
}
return GalleryRecord{}, fmt.Errorf("create gallery: %w", err)
}
return r.GetForUser(ctx, userID, id)
}
func (r *Repository) ListForUser(ctx context.Context, userID uuid.UUID) ([]Summary, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT
g.id, g.slug, g.title, g.client_name, g.description, g.status,
g.downloads_enabled, g.favorites_enabled, g.download_all_enabled,
g.watermark_enabled, g.expires_at, g.cover_media_id,
g.theme_config, g.branding_config, g.created_at, g.published_at,
COUNT(CASE WHEN m.mime_type LIKE 'image/%' THEN 1 END),
COUNT(CASE WHEN m.mime_type LIKE 'video/%' THEN 1 END),
COALESCE(SUM(m.file_size), 0)
FROM galleries g
LEFT JOIN media m ON m.gallery_id = g.id
WHERE g.user_id = $1 AND g.status <> 'archived'
GROUP BY g.id
ORDER BY g.created_at DESC
`, userID)
if err != nil {
return nil, fmt.Errorf("list galleries: %w", err)
}
defer rows.Close()
result := make([]Summary, 0)
for rows.Next() {
var (
item Summary
expiresAt, coverMediaID, createdAt sql.NullString
publishedAt sql.NullString
themeConfig, brandingConfig []byte
)
if err := rows.Scan(
&item.ID, &item.Slug, &item.Title, &item.ClientName, &item.Description, &item.Status,
&item.DownloadsEnabled, &item.FavoritesEnabled, &item.DownloadAllEnabled,
&item.WatermarkEnabled, &expiresAt, &coverMediaID, &themeConfig, &brandingConfig,
&createdAt, &publishedAt, &item.PhotoCount, &item.VideoCount, &item.TotalBytes,
); err != nil {
return nil, fmt.Errorf("scan gallery summary: %w", err)
}
item.ExpiresAt = expiresAt.String
item.CoverMediaID = coverMediaID.String
item.ThemeConfig = nonEmptyJSON(themeConfig)
item.BrandingConfig = nonEmptyJSON(brandingConfig)
item.CreatedAt = createdAt.String
item.PublishedAt = publishedAt.String
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate galleries: %w", err)
}
return result, nil
}
func (r *Repository) GetForUser(ctx context.Context, userID, galleryID uuid.UUID) (GalleryRecord, error) {
return r.get(ctx, `
SELECT id, user_id, slug, title, client_name, description, status, password_hash,
downloads_enabled, favorites_enabled, download_all_enabled, watermark_enabled,
expires_at, cover_media_id, theme_config, branding_config, created_at, updated_at, published_at
FROM galleries
WHERE id = $1 AND user_id = $2
`, galleryID, userID)
}
func (r *Repository) GetPublicBySlug(ctx context.Context, slug string) (GalleryRecord, error) {
return r.get(ctx, `
SELECT id, user_id, slug, title, client_name, description, status, password_hash,
downloads_enabled, favorites_enabled, download_all_enabled, watermark_enabled,
expires_at, cover_media_id, theme_config, branding_config, created_at, updated_at, published_at
FROM galleries
WHERE slug = $1 AND status = 'published'
`, slug)
}
func (r *Repository) Update(ctx context.Context, userID, galleryID uuid.UUID, input UpdateInput) (GalleryRecord, error) {
var passwordValue any
if input.ClearPassword {
passwordValue = nil
} else if input.PasswordHash != nil {
passwordValue = *input.PasswordHash
} else {
current, err := r.GetForUser(ctx, userID, galleryID)
if err != nil {
return GalleryRecord{}, err
}
passwordValue = current.PasswordHash
}
var expiresValue any
if input.ExpiresAt != nil && strings.TrimSpace(*input.ExpiresAt) != "" {
expiresValue = *input.ExpiresAt
}
var coverValue any
if input.CoverMediaID != nil && strings.TrimSpace(*input.CoverMediaID) != "" {
coverID, err := uuid.Parse(strings.TrimSpace(*input.CoverMediaID))
if err != nil {
return GalleryRecord{}, fmt.Errorf("parse cover media id: %w", err)
}
coverValue = coverID
}
_, err := r.db.ExecContext(ctx, `
UPDATE galleries
SET title = $1, client_name = $2, description = $3,
password_hash = $4, downloads_enabled = $5, favorites_enabled = $6,
download_all_enabled = $7, watermark_enabled = $8, expires_at = $9,
cover_media_id = $10, theme_config = $11, branding_config = $12,
updated_at = CURRENT_TIMESTAMP
WHERE id = $13 AND user_id = $14
`, input.Title, input.ClientName, input.Description, passwordValue, input.DownloadsEnabled,
input.FavoritesEnabled, input.DownloadAllEnabled, input.WatermarkEnabled, expiresValue,
coverValue, jsonValue(input.ThemeConfig), jsonValue(input.BrandingConfig), galleryID, userID)
if err != nil {
return GalleryRecord{}, fmt.Errorf("update gallery: %w", err)
}
return r.GetForUser(ctx, userID, galleryID)
}
func (r *Repository) SetStatus(ctx context.Context, userID, galleryID uuid.UUID, status string) (GalleryRecord, error) {
var query string
if status == StatusPublished {
query = `UPDATE galleries SET status = $1, published_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND user_id = $3`
} else {
query = `UPDATE galleries SET status = $1, published_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND user_id = $3`
}
_, err := r.db.ExecContext(ctx, query, status, galleryID, userID)
if err != nil {
return GalleryRecord{}, fmt.Errorf("set gallery status: %w", err)
}
return r.GetForUser(ctx, userID, galleryID)
}
func (r *Repository) Delete(ctx context.Context, userID, galleryID uuid.UUID) error {
result, err := r.db.ExecContext(ctx, `DELETE FROM galleries WHERE id = $1 AND user_id = $2`, galleryID, userID)
if err != nil {
return fmt.Errorf("delete gallery: %w", err)
}
count, err := result.RowsAffected()
if err != nil || count == 0 {
return ErrNotFound
}
return nil
}
type rowScanner interface {
Scan(...any) error
}
func (r *Repository) get(ctx context.Context, query string, args ...any) (GalleryRecord, error) {
return scanGallery(r.db.QueryRowContext(ctx, query, args...))
}
func scanGallery(row rowScanner) (GalleryRecord, error) {
var (
gallery GalleryRecord
passwordHash, expiresAt, coverMediaID sql.NullString
themeConfig, brandingConfig []byte
createdAt, updatedAt, publishedAt sql.NullString
)
err := row.Scan(
&gallery.ID, &gallery.UserID, &gallery.Slug, &gallery.Title, &gallery.ClientName,
&gallery.Description, &gallery.Status, &passwordHash, &gallery.DownloadsEnabled,
&gallery.FavoritesEnabled, &gallery.DownloadAllEnabled, &gallery.WatermarkEnabled,
&expiresAt, &coverMediaID, &themeConfig, &brandingConfig, &createdAt, &updatedAt, &publishedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return GalleryRecord{}, ErrNotFound
}
if err != nil {
return GalleryRecord{}, fmt.Errorf("scan gallery: %w", err)
}
gallery.PasswordHash = passwordHash.String
gallery.ExpiresAt = expiresAt.String
gallery.CoverMediaID = coverMediaID.String
gallery.ThemeConfig = nonEmptyJSON(themeConfig)
gallery.BrandingConfig = nonEmptyJSON(brandingConfig)
gallery.CreatedAt = createdAt.String
gallery.UpdatedAt = updatedAt.String
gallery.PublishedAt = publishedAt.String
return gallery, nil
}
func nonEmptyJSON(value []byte) json.RawMessage {
if len(value) == 0 {
return json.RawMessage(`{}`)
}
return json.RawMessage(value)
}
func jsonValue(value json.RawMessage) string {
if len(value) == 0 {
return `{}`
}
return string(value)
}