ai slop ah
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/example/sndit/backend/internal/auth"
|
||||
"github.com/example/sndit/backend/internal/storage"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -35,14 +36,14 @@ func NewHandler(repository *Repository, objectStorage storage.Storage, processor
|
||||
return &Handler{repository: repository, storage: objectStorage, processor: processor, auth: authService}
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterRoutes(mux *http.ServeMux, require func(http.Handler) http.Handler) {
|
||||
mux.Handle("GET /api/galleries/{id}/media", require(http.HandlerFunc(h.List)))
|
||||
mux.Handle("POST /api/galleries/{id}/uploads", require(http.HandlerFunc(h.CreateUpload)))
|
||||
mux.Handle("POST /api/uploads/{id}/complete", require(http.HandlerFunc(h.CompleteUpload)))
|
||||
mux.Handle("PATCH /api/media/{id}", require(http.HandlerFunc(h.Update)))
|
||||
mux.Handle("POST /api/media/{id}/download", require(http.HandlerFunc(h.Download)))
|
||||
mux.Handle("DELETE /api/uploads/{id}", require(http.HandlerFunc(h.Delete)))
|
||||
mux.Handle("DELETE /api/media/{id}", require(http.HandlerFunc(h.Delete)))
|
||||
func (h *Handler) RegisterRoutes(router gin.IRouter, require gin.HandlerFunc) {
|
||||
router.GET("/api/galleries/:id/media", require, h.List)
|
||||
router.POST("/api/galleries/:id/uploads", require, h.CreateUpload)
|
||||
router.POST("/api/uploads/:id/complete", require, h.CompleteUpload)
|
||||
router.PATCH("/api/media/:id", require, h.Update)
|
||||
router.POST("/api/media/:id/download", require, h.Download)
|
||||
router.DELETE("/api/uploads/:id", require, h.Delete)
|
||||
router.DELETE("/api/media/:id", require, h.Delete)
|
||||
}
|
||||
|
||||
type uploadRequest struct {
|
||||
@@ -55,57 +56,82 @@ type updateRequest struct {
|
||||
SortOrder *int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
// List godoc
|
||||
// @Summary List gallery media
|
||||
// @Tags media
|
||||
// @Produce json
|
||||
// @Security studioSession
|
||||
// @Param id path string true "Gallery UUID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/galleries/{id}/media [get]
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
user, ok := auth.UserFromContext(c)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
writeError(c, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
galleryID, err := parseID(r.PathValue("id"))
|
||||
galleryID, err := parseID(c.Param("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid gallery id")
|
||||
writeError(c, http.StatusBadRequest, "invalid gallery id")
|
||||
return
|
||||
}
|
||||
belongs, err := h.repository.GalleryBelongsToUser(r.Context(), galleryID, user.ID)
|
||||
belongs, err := h.repository.GalleryBelongsToUser(c.Request.Context(), galleryID, user.ID)
|
||||
if err != nil || !belongs {
|
||||
writeError(w, http.StatusNotFound, "gallery not found")
|
||||
writeError(c, http.StatusNotFound, "gallery not found")
|
||||
return
|
||||
}
|
||||
items, err := h.repository.ListByGallery(r.Context(), galleryID)
|
||||
items, err := h.repository.ListByGallery(c.Request.Context(), galleryID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not load media")
|
||||
writeError(c, http.StatusInternalServerError, "could not load media")
|
||||
return
|
||||
}
|
||||
views := make([]Public, 0, len(items))
|
||||
for _, item := range items {
|
||||
view, err := h.view(r.Context(), item, true)
|
||||
view, err := h.view(c.Request.Context(), item, true)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not sign media URLs")
|
||||
writeError(c, http.StatusInternalServerError, "could not sign media URLs")
|
||||
return
|
||||
}
|
||||
views = append(views, view)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"media": views})
|
||||
writeJSON(c, http.StatusOK, map[string]any{"media": views})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateUpload(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
// CreateUpload godoc
|
||||
// @Summary Create a presigned media upload
|
||||
// @Tags media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security studioSession
|
||||
// @Param id path string true "Gallery UUID"
|
||||
// @Param request body uploadRequest true "Upload metadata"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/galleries/{id}/uploads [post]
|
||||
func (h *Handler) CreateUpload(c *gin.Context) {
|
||||
user, ok := auth.UserFromContext(c)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
writeError(c, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
galleryID, err := parseID(r.PathValue("id"))
|
||||
galleryID, err := parseID(c.Param("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid gallery id")
|
||||
writeError(c, http.StatusBadRequest, "invalid gallery id")
|
||||
return
|
||||
}
|
||||
belongs, err := h.repository.GalleryBelongsToUser(r.Context(), galleryID, user.ID)
|
||||
belongs, err := h.repository.GalleryBelongsToUser(c.Request.Context(), galleryID, user.ID)
|
||||
if err != nil || !belongs {
|
||||
writeError(w, http.StatusNotFound, "gallery not found")
|
||||
writeError(c, http.StatusNotFound, "gallery not found")
|
||||
return
|
||||
}
|
||||
var request uploadRequest
|
||||
if !decodeJSON(w, r, &request) {
|
||||
if !decodeJSON(c, &request) {
|
||||
return
|
||||
}
|
||||
filename := safeFilename(request.Filename)
|
||||
@@ -114,145 +140,192 @@ func (h *Handler) CreateUpload(w http.ResponseWriter, r *http.Request) {
|
||||
mimeType = mime.TypeByExtension(filepath.Ext(filename))
|
||||
}
|
||||
if filename == "" || len(filename) > 255 || !allowedMimeType(mimeType) {
|
||||
writeError(w, http.StatusBadRequest, "unsupported media file")
|
||||
writeError(c, http.StatusBadRequest, "unsupported media file")
|
||||
return
|
||||
}
|
||||
if request.FileSize <= 0 || request.FileSize > maxUploadSize {
|
||||
writeError(w, http.StatusBadRequest, "file size must be between 1 byte and 10 GB")
|
||||
writeError(c, http.StatusBadRequest, "file size must be between 1 byte and 10 GB")
|
||||
return
|
||||
}
|
||||
mediaID := uuid.New()
|
||||
storageKey := fmt.Sprintf("galleries/%s/%s/original/%s", galleryID, mediaID, filename)
|
||||
item, err := h.repository.Create(r.Context(), galleryID, mediaID, filename, mimeType, request.FileSize, storageKey)
|
||||
item, err := h.repository.Create(c.Request.Context(), galleryID, mediaID, filename, mimeType, request.FileSize, storageKey)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not create upload")
|
||||
writeError(c, http.StatusInternalServerError, "could not create upload")
|
||||
return
|
||||
}
|
||||
uploadURL, err := h.storage.CreateUploadURL(r.Context(), storageKey, mimeType, uploadURLDuration)
|
||||
uploadURL, err := h.storage.CreateUploadURL(c.Request.Context(), storageKey, mimeType, uploadURLDuration)
|
||||
if err != nil {
|
||||
_, _ = h.repository.Delete(r.Context(), user.ID, mediaID)
|
||||
writeError(w, http.StatusInternalServerError, "could not create upload URL")
|
||||
_, _ = h.repository.Delete(c.Request.Context(), user.ID, mediaID)
|
||||
writeError(c, http.StatusInternalServerError, "could not create upload URL")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]any{
|
||||
writeJSON(c, http.StatusCreated, map[string]any{
|
||||
"uploadId": mediaID.String(),
|
||||
"uploadUrl": uploadURL,
|
||||
"media": publicFromRecord(item),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) CompleteUpload(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
// CompleteUpload godoc
|
||||
// @Summary Complete a media upload
|
||||
// @Tags media
|
||||
// @Produce json
|
||||
// @Security studioSession
|
||||
// @Param id path string true "Media UUID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/uploads/{id}/complete [post]
|
||||
func (h *Handler) CompleteUpload(c *gin.Context) {
|
||||
user, ok := auth.UserFromContext(c)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
writeError(c, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
mediaID, err := parseID(r.PathValue("id"))
|
||||
mediaID, err := parseID(c.Param("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid media id")
|
||||
writeError(c, http.StatusBadRequest, "invalid media id")
|
||||
return
|
||||
}
|
||||
item, err := h.repository.GetForUser(r.Context(), user.ID, mediaID)
|
||||
item, err := h.repository.GetForUser(c.Request.Context(), user.ID, mediaID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "media not found")
|
||||
writeError(c, http.StatusNotFound, "media not found")
|
||||
return
|
||||
}
|
||||
info, err := h.storage.Stat(r.Context(), item.StorageKey)
|
||||
info, err := h.storage.Stat(c.Request.Context(), item.StorageKey)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "uploaded object is not available yet")
|
||||
writeError(c, http.StatusBadRequest, "uploaded object is not available yet")
|
||||
return
|
||||
}
|
||||
item, err = h.repository.Complete(r.Context(), user.ID, mediaID, info.Size)
|
||||
item, err = h.repository.Complete(c.Request.Context(), user.ID, mediaID, info.Size)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not complete upload")
|
||||
writeError(c, http.StatusInternalServerError, "could not complete upload")
|
||||
return
|
||||
}
|
||||
h.processor.Enqueue(item.ID)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"media": publicFromRecord(item)})
|
||||
writeJSON(c, http.StatusOK, map[string]any{"media": publicFromRecord(item)})
|
||||
}
|
||||
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
// Update godoc
|
||||
// @Summary Update media ordering
|
||||
// @Tags media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security studioSession
|
||||
// @Param id path string true "Media UUID"
|
||||
// @Param request body updateRequest true "Media changes"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/media/{id} [patch]
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
user, ok := auth.UserFromContext(c)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
writeError(c, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
mediaID, err := parseID(r.PathValue("id"))
|
||||
mediaID, err := parseID(c.Param("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid media id")
|
||||
writeError(c, http.StatusBadRequest, "invalid media id")
|
||||
return
|
||||
}
|
||||
var request updateRequest
|
||||
if !decodeJSON(w, r, &request) {
|
||||
if !decodeJSON(c, &request) {
|
||||
return
|
||||
}
|
||||
if request.SortOrder == nil || *request.SortOrder < 0 {
|
||||
writeError(w, http.StatusBadRequest, "sort order must be zero or greater")
|
||||
writeError(c, http.StatusBadRequest, "sort order must be zero or greater")
|
||||
return
|
||||
}
|
||||
if err := h.repository.UpdateSortOrder(r.Context(), user.ID, mediaID, *request.SortOrder); err != nil {
|
||||
writeError(w, http.StatusNotFound, "media not found")
|
||||
if err := h.repository.UpdateSortOrder(c.Request.Context(), user.ID, mediaID, *request.SortOrder); err != nil {
|
||||
writeError(c, http.StatusNotFound, "media not found")
|
||||
return
|
||||
}
|
||||
item, _ := h.repository.GetForUser(r.Context(), user.ID, mediaID)
|
||||
view, err := h.view(r.Context(), item, true)
|
||||
item, _ := h.repository.GetForUser(c.Request.Context(), user.ID, mediaID)
|
||||
view, err := h.view(c.Request.Context(), item, true)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not sign media URL")
|
||||
writeError(c, http.StatusInternalServerError, "could not sign media URL")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"media": view})
|
||||
writeJSON(c, http.StatusOK, map[string]any{"media": view})
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
// Delete godoc
|
||||
// @Summary Delete media or cancel an upload
|
||||
// @Tags media
|
||||
// @Produce json
|
||||
// @Security studioSession
|
||||
// @Param id path string true "Media UUID"
|
||||
// @Success 204
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/uploads/{id} [delete]
|
||||
// @Router /api/media/{id} [delete]
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
user, ok := auth.UserFromContext(c)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
writeError(c, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
mediaID, err := parseID(r.PathValue("id"))
|
||||
mediaID, err := parseID(c.Param("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid media id")
|
||||
writeError(c, http.StatusBadRequest, "invalid media id")
|
||||
return
|
||||
}
|
||||
item, err := h.repository.Delete(r.Context(), user.ID, mediaID)
|
||||
item, err := h.repository.Delete(c.Request.Context(), user.ID, mediaID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "media not found")
|
||||
writeError(c, http.StatusNotFound, "media not found")
|
||||
return
|
||||
}
|
||||
for _, key := range []string{item.StorageKey, item.PreviewKey, item.ThumbnailKey} {
|
||||
if key != "" && key != item.ExternalURL {
|
||||
_ = h.storage.Delete(r.Context(), key)
|
||||
_ = h.storage.Delete(c.Request.Context(), key)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
// Download godoc
|
||||
// @Summary Create a signed original media download URL
|
||||
// @Tags media
|
||||
// @Produce json
|
||||
// @Security studioSession
|
||||
// @Param id path string true "Media UUID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/media/{id}/download [post]
|
||||
func (h *Handler) Download(c *gin.Context) {
|
||||
user, ok := auth.UserFromContext(c)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
writeError(c, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
mediaID, err := parseID(r.PathValue("id"))
|
||||
mediaID, err := parseID(c.Param("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid media id")
|
||||
writeError(c, http.StatusBadRequest, "invalid media id")
|
||||
return
|
||||
}
|
||||
item, err := h.repository.GetForUser(r.Context(), user.ID, mediaID)
|
||||
item, err := h.repository.GetForUser(c.Request.Context(), user.ID, mediaID)
|
||||
if err != nil || item.ProcessingStatus != StatusReady {
|
||||
writeError(w, http.StatusNotFound, "media not found")
|
||||
writeError(c, http.StatusNotFound, "media not found")
|
||||
return
|
||||
}
|
||||
url := item.ExternalURL
|
||||
if url == "" {
|
||||
url, err = h.storage.CreateDownloadURL(r.Context(), item.StorageKey, time.Hour)
|
||||
url, err = h.storage.CreateDownloadURL(c.Request.Context(), item.StorageKey, time.Hour)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not create download")
|
||||
writeError(c, http.StatusInternalServerError, "could not create download")
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = h.repository.RecordDownload(r.Context(), item.GalleryID, &item.ID, user.ID.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"url": url})
|
||||
_ = h.repository.RecordDownload(c.Request.Context(), item.GalleryID, &item.ID, user.ID.String())
|
||||
writeJSON(c, http.StatusOK, map[string]string{"url": url})
|
||||
}
|
||||
|
||||
func (h *Handler) view(ctx context.Context, item Record, includeOriginal bool) (Public, error) {
|
||||
@@ -330,26 +403,24 @@ func parseID(value string) (uuid.UUID, error) {
|
||||
return uuid.Parse(value)
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, target any) bool {
|
||||
if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "content type must be application/json")
|
||||
func decodeJSON(c *gin.Context, target any) bool {
|
||||
if !strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") {
|
||||
writeError(c, http.StatusUnsupportedMediaType, "content type must be application/json")
|
||||
return false
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(target); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
writeError(c, http.StatusBadRequest, "invalid JSON body")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]string{"error": message})
|
||||
func writeError(c *gin.Context, status int, message string) {
|
||||
writeJSON(c, 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)
|
||||
func writeJSON(c *gin.Context, status int, value any) {
|
||||
c.JSON(status, value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user