231 lines
8.0 KiB
Go
231 lines
8.0 KiB
Go
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)
|
|
}
|