package downloads import ( "encoding/json" "net/http" "strings" "time" "github.com/example/sndit/backend/internal/auth" "github.com/example/sndit/backend/internal/galleries" "github.com/example/sndit/backend/internal/media" "github.com/example/sndit/backend/internal/storage" "github.com/google/uuid" ) type Handler struct { galleries *galleries.Repository media *media.Repository storage storage.Storage auth *auth.Service service *Service } func NewHandler(galleryRepository *galleries.Repository, mediaRepository *media.Repository, objectStorage storage.Storage, authService *auth.Service, service *Service) *Handler { return &Handler{galleries: galleryRepository, media: mediaRepository, storage: objectStorage, auth: authService, service: service} } func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /api/public/galleries/{slug}/media/{mediaId}/download", h.Download) mux.HandleFunc("POST /api/public/galleries/{slug}/download-all", h.DownloadAll) mux.HandleFunc("GET /api/public/galleries/{slug}/download-all/{jobId}", h.DownloadAllStatus) } func (h *Handler) Download(w http.ResponseWriter, r *http.Request) { record, err := h.publicRecord(r) if err != nil { writeError(w, http.StatusNotFound, "gallery not found") return } if !record.DownloadsEnabled { writeError(w, http.StatusForbidden, "downloads are disabled") return } mediaID, err := uuid.Parse(r.PathValue("mediaId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid media id") return } item, err := h.media.GetByID(r.Context(), mediaID) if err != nil || item.GalleryID != record.ID || item.ProcessingStatus != media.StatusReady { writeError(w, http.StatusNotFound, "media not found") return } url, err := h.downloadURL(r, item) if err != nil { writeError(w, http.StatusInternalServerError, "could not create download") return } visitorID := h.auth.EnsureVisitor(w, r) _ = h.media.RecordDownload(r.Context(), record.ID, &mediaID, visitorID) writeJSON(w, http.StatusOK, map[string]string{"url": url}) } func (h *Handler) DownloadAll(w http.ResponseWriter, r *http.Request) { record, err := h.publicRecord(r) if err != nil { writeError(w, http.StatusNotFound, "gallery not found") return } if !record.DownloadAllEnabled || !record.DownloadsEnabled { writeError(w, http.StatusForbidden, "gallery downloads are disabled") return } visitorID := h.auth.EnsureVisitor(w, r) job, err := h.service.Create(r.Context(), record.ID, visitorID) if err != nil { writeError(w, http.StatusInternalServerError, "could not start gallery download") return } writeJSON(w, http.StatusAccepted, map[string]string{"jobId": job.ID.String(), "status": job.Status}) } func (h *Handler) DownloadAllStatus(w http.ResponseWriter, r *http.Request) { record, err := h.publicRecord(r) if err != nil { writeError(w, http.StatusNotFound, "gallery not found") return } if !record.DownloadAllEnabled || !record.DownloadsEnabled { writeError(w, http.StatusForbidden, "gallery downloads are disabled") return } jobID, err := uuid.Parse(r.PathValue("jobId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid download job id") return } visitorID := h.auth.EnsureVisitor(w, r) job, err := h.service.Get(r.Context(), jobID, record.ID, visitorID) if err != nil { writeError(w, http.StatusNotFound, "download job not found") return } response := map[string]any{"jobId": job.ID.String(), "status": job.Status} if job.Error != "" { response["error"] = job.Error } if job.Status == StatusReady { url, err := h.storage.CreateDownloadURL(r.Context(), job.StorageKey, time.Hour) if err != nil { writeError(w, http.StatusInternalServerError, "could not create download URL") return } response["url"] = url } writeJSON(w, http.StatusOK, response) } func (h *Handler) publicRecord(r *http.Request) (galleries.GalleryRecord, error) { record, err := h.galleries.GetPublicBySlug(r.Context(), strings.TrimSpace(r.PathValue("slug"))) if err != nil || record.IsExpired() { return galleries.GalleryRecord{}, galleries.ErrNotFound } if record.PasswordHash != "" && !h.auth.HasGalleryAccess(r, record.Slug) { return galleries.GalleryRecord{}, galleries.ErrNotFound } return record, nil } func (h *Handler) downloadURL(r *http.Request, item media.Record) (string, error) { if item.ExternalURL != "" { return item.ExternalURL, nil } return h.storage.CreateDownloadURL(r.Context(), item.StorageKey, time.Hour) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) } func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) }