This commit is contained in:
2026-08-22 02:59:16 +02:00
commit 6a5bb1d699
100 changed files with 17409 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
package media
import (
"context"
"database/sql"
"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("media not found")
func (r *Repository) Create(ctx context.Context, galleryID, id uuid.UUID, filename, mimeType string, fileSize int64, storageKey string) (Record, error) {
_, err := r.db.ExecContext(ctx, `
INSERT INTO media (id, gallery_id, original_filename, mime_type, file_size, storage_key, processing_status, sort_order)
VALUES ($1, $2, $3, $4, $5, $6, $7, COALESCE((SELECT MAX(sort_order) + 1 FROM media WHERE gallery_id = $2), 0))
`, id, galleryID, filename, mimeType, fileSize, storageKey, StatusUploading)
if err != nil {
return Record{}, fmt.Errorf("create media: %w", err)
}
return r.GetByID(ctx, id)
}
func (r *Repository) GalleryBelongsToUser(ctx context.Context, galleryID, userID uuid.UUID) (bool, error) {
var exists bool
err := r.db.QueryRowContext(ctx, `
SELECT EXISTS (SELECT 1 FROM galleries WHERE id = $1 AND user_id = $2 AND status <> 'archived')
`, galleryID, userID).Scan(&exists)
return exists, err
}
func (r *Repository) GetByID(ctx context.Context, id uuid.UUID) (Record, error) {
return r.get(ctx, `
SELECT id, gallery_id, original_filename, mime_type, file_size, storage_key,
external_url, thumbnail_key, preview_key, processing_status, processing_error,
width, height, duration_seconds, sort_order, created_at, updated_at
FROM media
WHERE id = $1
`, id)
}
func (r *Repository) GetForUser(ctx context.Context, userID, mediaID uuid.UUID) (Record, error) {
return r.get(ctx, `
SELECT m.id, m.gallery_id, m.original_filename, m.mime_type, m.file_size, m.storage_key,
m.external_url, m.thumbnail_key, m.preview_key, m.processing_status, m.processing_error,
m.width, m.height, m.duration_seconds, m.sort_order, m.created_at, m.updated_at
FROM media m
JOIN galleries g ON g.id = m.gallery_id
WHERE m.id = $1 AND g.user_id = $2
`, mediaID, userID)
}
func (r *Repository) ListByGallery(ctx context.Context, galleryID uuid.UUID) ([]Record, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, gallery_id, original_filename, mime_type, file_size, storage_key,
external_url, thumbnail_key, preview_key, processing_status, processing_error,
width, height, duration_seconds, sort_order, created_at, updated_at
FROM media
WHERE gallery_id = $1
ORDER BY sort_order ASC, id ASC
`, galleryID)
if err != nil {
return nil, fmt.Errorf("list media: %w", err)
}
defer rows.Close()
result := make([]Record, 0)
for rows.Next() {
item, err := scanMedia(rows)
if err != nil {
return nil, err
}
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate media: %w", err)
}
return result, nil
}
func (r *Repository) Complete(ctx context.Context, userID, mediaID uuid.UUID, fileSize int64) (Record, error) {
result, err := r.db.ExecContext(ctx, `
UPDATE media
SET file_size = $1, processing_status = $2, processing_error = NULL, updated_at = CURRENT_TIMESTAMP
WHERE id = $3 AND gallery_id IN (SELECT id FROM galleries WHERE user_id = $4)
`, fileSize, StatusProcessing, mediaID, userID)
if err != nil {
return Record{}, fmt.Errorf("complete media upload: %w", err)
}
if count, _ := result.RowsAffected(); count == 0 {
return Record{}, ErrNotFound
}
return r.GetByID(ctx, mediaID)
}
func (r *Repository) MarkReady(ctx context.Context, mediaID uuid.UUID, previewKey, thumbnailKey string, width, height int) error {
_, err := r.db.ExecContext(ctx, `
UPDATE media
SET processing_status = $1, processing_error = NULL, preview_key = $2, thumbnail_key = $3,
width = $4, height = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = $6
`, StatusReady, previewKey, thumbnailKey, width, height, mediaID)
return err
}
func (r *Repository) MarkFailed(ctx context.Context, mediaID uuid.UUID, message string) error {
_, err := r.db.ExecContext(ctx, `
UPDATE media
SET processing_status = $1, processing_error = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $3
`, StatusFailed, message, mediaID)
return err
}
func (r *Repository) UpdateSortOrder(ctx context.Context, userID, mediaID uuid.UUID, sortOrder int) error {
result, err := r.db.ExecContext(ctx, `
UPDATE media
SET sort_order = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2 AND gallery_id IN (SELECT id FROM galleries WHERE user_id = $3)
`, sortOrder, mediaID, userID)
if err != nil {
return fmt.Errorf("update media order: %w", err)
}
if count, _ := result.RowsAffected(); count == 0 {
return ErrNotFound
}
return nil
}
func (r *Repository) Delete(ctx context.Context, userID, mediaID uuid.UUID) (Record, error) {
item, err := r.GetForUser(ctx, userID, mediaID)
if err != nil {
return Record{}, err
}
if _, err := r.db.ExecContext(ctx, `DELETE FROM media WHERE id = $1`, mediaID); err != nil {
return Record{}, fmt.Errorf("delete media: %w", err)
}
return item, nil
}
func (r *Repository) IsFavorited(ctx context.Context, galleryID, mediaID uuid.UUID, visitorID string) (bool, error) {
var exists bool
err := r.db.QueryRowContext(ctx, `
SELECT EXISTS (SELECT 1 FROM favorites WHERE gallery_id = $1 AND media_id = $2 AND visitor_id = $3)
`, galleryID, mediaID, visitorID).Scan(&exists)
return exists, err
}
func (r *Repository) SetFavorite(ctx context.Context, galleryID, mediaID uuid.UUID, visitorID string, favorited bool) error {
if favorited {
_, err := r.db.ExecContext(ctx, `
INSERT INTO favorites (id, gallery_id, media_id, visitor_id)
VALUES ($1, $2, $3, $4)
ON CONFLICT (gallery_id, media_id, visitor_id) DO NOTHING
`, uuid.New(), galleryID, mediaID, visitorID)
return err
}
_, err := r.db.ExecContext(ctx, `DELETE FROM favorites WHERE gallery_id = $1 AND media_id = $2 AND visitor_id = $3`, galleryID, mediaID, visitorID)
return err
}
func (r *Repository) RecordDownload(ctx context.Context, galleryID uuid.UUID, mediaID *uuid.UUID, visitorID string) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO downloads (id, gallery_id, media_id, visitor_id)
VALUES ($1, $2, $3, $4)
`, uuid.New(), galleryID, mediaID, visitorID)
return err
}
type rowScanner interface {
Scan(...any) error
}
func (r *Repository) get(ctx context.Context, query string, args ...any) (Record, error) {
return scanMedia(r.db.QueryRowContext(ctx, query, args...))
}
func scanMedia(row rowScanner) (Record, error) {
var (
item Record
externalURL, thumbnailKey, previewKey, processingError sql.NullString
width, height sql.NullInt64
duration sql.NullFloat64
createdAt, updatedAt sql.NullString
)
err := row.Scan(
&item.ID, &item.GalleryID, &item.OriginalFilename, &item.MimeType, &item.FileSize,
&item.StorageKey, &externalURL, &thumbnailKey, &previewKey, &item.ProcessingStatus,
&processingError, &width, &height, &duration, &item.SortOrder, &createdAt, &updatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return Record{}, ErrNotFound
}
if err != nil {
return Record{}, fmt.Errorf("scan media: %w", err)
}
item.ExternalURL = externalURL.String
item.ThumbnailKey = thumbnailKey.String
item.PreviewKey = previewKey.String
item.ProcessingError = processingError.String
item.Width = int(width.Int64)
item.Height = int(height.Int64)
item.DurationSeconds = duration.Float64
item.CreatedAt = createdAt.String
item.UpdatedAt = updatedAt.String
return item, nil
}
func IsImage(item Record) bool {
return strings.HasPrefix(strings.ToLower(item.MimeType), "image/")
}
func IsVideo(item Record) bool {
return strings.HasPrefix(strings.ToLower(item.MimeType), "video/")
}