226 lines
7.7 KiB
Go
226 lines
7.7 KiB
Go
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/")
|
|
}
|