package api import ( "fmt" "io" "net/http" "strings" "codeberg.org/maxwelljensen/weimar/internal/db" "codeberg.org/maxwelljensen/weimar/internal/storage" ) func (a *API) HandleUpload(w http.ResponseWriter, r *http.Request) { user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "unauthorized") return } if err := r.ParseMultipartForm(32 << 20); err != nil { writeError(w, http.StatusBadRequest, "invalid multipart form") return } file, header, err := r.FormFile("file") if err != nil { writeError(w, http.StatusBadRequest, "file field is required") return } defer file.Close() // Read uploaded data. data, err := io.ReadAll(file) if err != nil { writeError(w, http.StatusInternalServerError, "failed to read file") return } // Validate file size against config. maxBytes := int64(a.Cfg.Upload.MaxSizeMB) * 1024 * 1024 if maxBytes > 0 && int64(len(data)) > maxBytes { writeError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("file exceeds max size of %d MB", a.Cfg.Upload.MaxSizeMB)) return } // Detect MIME type from magic bytes. mimeType := storage.DetectContentType(data) if !storage.IsImage(mimeType) && !storage.IsVideo(mimeType) { writeError(w, http.StatusBadRequest, "unsupported file type") return } ext := storage.ExtensionFromMIME(mimeType) // Process the data: strip EXIF for images. var processed []byte if storage.IsImage(mimeType) { stripped, err := storage.StripEXIF(data) if err != nil { writeError(w, http.StatusInternalServerError, "failed to process image") return } processed = stripped } else { processed = data } // Store original to disk. _, storagePath, err := a.Storage.Store(processed, ext) if err != nil { writeError(w, http.StatusInternalServerError, "failed to store file") return } // Get image dimensions (images only). var width, height int if storage.IsImage(mimeType) { img, _, _ := storage.DecodeImage(processed) if img != nil { bounds := img.Bounds() width = bounds.Dx() height = bounds.Dy() } } // Generate thumbnail. var thumbnailPath *string if storage.IsImage(mimeType) { thumbData, err := storage.GenerateThumbnail(processed, storage.ThumbnailMaxDimension) if err == nil { thumbRel := storage.ThumbnailPath(storagePath) if err := a.Storage.StoreAt(thumbRel, thumbData); err == nil { thumbnailPath = &thumbRel } } } else if storage.IsVideo(mimeType) && storage.FFmpegAvailable() { thumbRel := storage.ThumbnailPath(storagePath) absPath := a.Storage.AbsPath(storagePath) thumbAbs := a.Storage.AbsPath(thumbRel) if err := storage.GenerateVideoThumbnail(absPath, thumbAbs); err == nil { thumbnailPath = &thumbRel } } // Read tags from form field(s). The frontend sends each tag as a // separate FormData entry (e.g., tags=foo&tags=bar) and a single // comma-separated string is also supported for REST clients. var tagList []string for _, v := range r.Form["tags"] { for _, t := range strings.Split(v, ",") { t = strings.TrimSpace(t) if t != "" { tagList = append(tagList, t) } } } // Compute innate tags based on media characteristics. videoPath := "" if storage.IsVideo(mimeType) { videoPath = a.Storage.AbsPath(storagePath) } innateTags := storage.ComputeInnateTags(mimeType, width, height, videoPath) // Create DB record. img, err := a.DB.CreateImage(r.Context(), &db.Image{ Filename: header.Filename, StoragePath: storagePath, ThumbnailPath: thumbnailPath, UploadedBy: user.ID, FileSize: int64(len(data)), Width: width, Height: height, MimeType: mimeType, InnateTags: innateTags, }) if err != nil { writeError(w, http.StatusInternalServerError, "failed to save metadata") return } // Set tags. if len(tagList) > 0 { if err := a.DB.SetImageTags(r.Context(), img.ID, tagList); err != nil { writeError(w, http.StatusInternalServerError, "failed to save tags") return } img.Tags = tagList } writeJSON(w, http.StatusCreated, img) }