init
This commit is contained in:
@@ -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/")
|
||||
}
|
||||
Reference in New Issue
Block a user