package downloads import ( "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/gin-gonic/gin" "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(router gin.IRouter) { router.POST("/api/public/galleries/:slug/media/:mediaId/download", h.Download) router.POST("/api/public/galleries/:slug/download-all", h.DownloadAll) router.GET("/api/public/galleries/:slug/download-all/:jobId", h.DownloadAllStatus) } // Download godoc // @Summary Download one public gallery media item // @Tags public downloads // @Produce json // @Param slug path string true "Gallery slug" // @Param mediaId path string true "Media UUID" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/public/galleries/{slug}/media/{mediaId}/download [post] func (h *Handler) Download(c *gin.Context) { record, err := h.publicRecord(c) if err != nil { writeError(c, http.StatusNotFound, "gallery not found") return } if !record.DownloadsEnabled { writeError(c, http.StatusForbidden, "downloads are disabled") return } mediaID, err := uuid.Parse(c.Param("mediaId")) if err != nil { writeError(c, http.StatusBadRequest, "invalid media id") return } item, err := h.media.GetByID(c.Request.Context(), mediaID) if err != nil || item.GalleryID != record.ID || item.ProcessingStatus != media.StatusReady { writeError(c, http.StatusNotFound, "media not found") return } url, err := h.downloadURL(c, item) if err != nil { writeError(c, http.StatusInternalServerError, "could not create download") return } visitorID := h.auth.EnsureVisitor(c) _ = h.media.RecordDownload(c.Request.Context(), record.ID, &mediaID, visitorID) writeJSON(c, http.StatusOK, map[string]string{"url": url}) } // DownloadAll godoc // @Summary Start a public gallery ZIP download // @Tags public downloads // @Produce json // @Param slug path string true "Gallery slug" // @Success 202 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/public/galleries/{slug}/download-all [post] func (h *Handler) DownloadAll(c *gin.Context) { record, err := h.publicRecord(c) if err != nil { writeError(c, http.StatusNotFound, "gallery not found") return } if !record.DownloadAllEnabled || !record.DownloadsEnabled { writeError(c, http.StatusForbidden, "gallery downloads are disabled") return } visitorID := h.auth.EnsureVisitor(c) job, err := h.service.Create(c.Request.Context(), record.ID, visitorID) if err != nil { writeError(c, http.StatusInternalServerError, "could not start gallery download") return } writeJSON(c, http.StatusAccepted, map[string]string{"jobId": job.ID.String(), "status": job.Status}) } // DownloadAllStatus godoc // @Summary Get public gallery ZIP download status // @Tags public downloads // @Produce json // @Param slug path string true "Gallery slug" // @Param jobId path string true "Download job UUID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/public/galleries/{slug}/download-all/{jobId} [get] func (h *Handler) DownloadAllStatus(c *gin.Context) { record, err := h.publicRecord(c) if err != nil { writeError(c, http.StatusNotFound, "gallery not found") return } if !record.DownloadAllEnabled || !record.DownloadsEnabled { writeError(c, http.StatusForbidden, "gallery downloads are disabled") return } jobID, err := uuid.Parse(c.Param("jobId")) if err != nil { writeError(c, http.StatusBadRequest, "invalid download job id") return } visitorID := h.auth.EnsureVisitor(c) job, err := h.service.Get(c.Request.Context(), jobID, record.ID, visitorID) if err != nil { writeError(c, 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(c.Request.Context(), job.StorageKey, time.Hour) if err != nil { writeError(c, http.StatusInternalServerError, "could not create download URL") return } response["url"] = url } writeJSON(c, http.StatusOK, response) } func (h *Handler) publicRecord(c *gin.Context) (galleries.GalleryRecord, error) { record, err := h.galleries.GetPublicBySlug(c.Request.Context(), strings.TrimSpace(c.Param("slug"))) if err != nil || record.IsExpired() { return galleries.GalleryRecord{}, galleries.ErrNotFound } if record.PasswordHash != "" && !h.auth.HasGalleryAccess(c, record.Slug) { return galleries.GalleryRecord{}, galleries.ErrNotFound } return record, nil } func (h *Handler) downloadURL(c *gin.Context, item media.Record) (string, error) { if item.ExternalURL != "" { return item.ExternalURL, nil } return h.storage.CreateDownloadURL(c.Request.Context(), item.StorageKey, time.Hour) } func writeError(c *gin.Context, status int, message string) { writeJSON(c, status, map[string]string{"error": message}) } func writeJSON(c *gin.Context, status int, value any) { c.JSON(status, value) }