all repos — weimar @ 3ab9da929b53350b9e5a860642317e5a8f5b3750

Unnamed repository; edit this file 'description' to name the repository.

internal/api/upload.go (view raw)

  1package api
  2
  3import (
  4	"fmt"
  5	"io"
  6	"net/http"
  7	"strings"
  8
  9	"github.com/mjensen/weimar/internal/db"
 10	"github.com/mjensen/weimar/internal/storage"
 11)
 12
 13func (a *API) HandleUpload(w http.ResponseWriter, r *http.Request) {
 14	user := UserFromContext(r.Context())
 15	if user == nil {
 16		writeError(w, http.StatusUnauthorized, "unauthorized")
 17		return
 18	}
 19
 20	if err := r.ParseMultipartForm(32 << 20); err != nil {
 21		writeError(w, http.StatusBadRequest, "invalid multipart form")
 22		return
 23	}
 24
 25	file, header, err := r.FormFile("file")
 26	if err != nil {
 27		writeError(w, http.StatusBadRequest, "file field is required")
 28		return
 29	}
 30	defer file.Close()
 31
 32	// Read uploaded data.
 33	data, err := io.ReadAll(file)
 34	if err != nil {
 35		writeError(w, http.StatusInternalServerError, "failed to read file")
 36		return
 37	}
 38
 39	// Validate file size against config.
 40	maxBytes := int64(a.Cfg.Upload.MaxSizeMB) * 1024 * 1024
 41	if maxBytes > 0 && int64(len(data)) > maxBytes {
 42		writeError(w, http.StatusRequestEntityTooLarge,
 43			fmt.Sprintf("file exceeds max size of %d MB", a.Cfg.Upload.MaxSizeMB))
 44		return
 45	}
 46
 47	// Detect MIME type from magic bytes.
 48	mimeType := storage.DetectContentType(data)
 49	if !storage.IsImage(mimeType) && !storage.IsVideo(mimeType) {
 50		writeError(w, http.StatusBadRequest, "unsupported file type")
 51		return
 52	}
 53
 54	ext := storage.ExtensionFromMIME(mimeType)
 55
 56	// Process the data: strip EXIF for images.
 57	var processed []byte
 58	if storage.IsImage(mimeType) {
 59		stripped, err := storage.StripEXIF(data)
 60		if err != nil {
 61			writeError(w, http.StatusInternalServerError, "failed to process image")
 62			return
 63		}
 64		processed = stripped
 65	} else {
 66		processed = data
 67	}
 68
 69	// Store original to disk.
 70	_, storagePath, err := a.Storage.Store(processed, ext)
 71	if err != nil {
 72		writeError(w, http.StatusInternalServerError, "failed to store file")
 73		return
 74	}
 75
 76	// Get image dimensions (images only).
 77	var width, height int
 78	if storage.IsImage(mimeType) {
 79		img, _, _ := storage.DecodeImage(processed)
 80		if img != nil {
 81			bounds := img.Bounds()
 82			width = bounds.Dx()
 83			height = bounds.Dy()
 84		}
 85	}
 86
 87	// Generate thumbnail.
 88	var thumbnailPath *string
 89	if storage.IsImage(mimeType) {
 90		thumbData, err := storage.GenerateThumbnail(processed, storage.ThumbnailMaxDimension)
 91		if err == nil {
 92			thumbRel := storage.ThumbnailPath(storagePath)
 93			if err := a.Storage.StoreAt(thumbRel, thumbData); err == nil {
 94				thumbnailPath = &thumbRel
 95			}
 96		}
 97	} else if storage.IsVideo(mimeType) && storage.FFmpegAvailable() {
 98		thumbRel := storage.ThumbnailPath(storagePath)
 99		absPath := a.Storage.AbsPath(storagePath)
100		thumbAbs := a.Storage.AbsPath(thumbRel)
101		if err := storage.GenerateVideoThumbnail(absPath, thumbAbs); err == nil {
102			thumbnailPath = &thumbRel
103		}
104	}
105
106	// Read tags from form field(s). The frontend sends each tag as a
107	// separate FormData entry (e.g., tags=foo&tags=bar) and a single
108	// comma-separated string is also supported for REST clients.
109	var tagList []string
110	for _, v := range r.Form["tags"] {
111		for _, t := range strings.Split(v, ",") {
112			t = strings.TrimSpace(t)
113			if t != "" {
114				tagList = append(tagList, t)
115			}
116		}
117	}
118
119	// Create DB record.
120	img, err := a.DB.CreateImage(r.Context(), &db.Image{
121		Filename:      header.Filename,
122		StoragePath:   storagePath,
123		ThumbnailPath: thumbnailPath,
124		UploadedBy:    user.ID,
125		FileSize:      int64(len(data)),
126		Width:         width,
127		Height:        height,
128		MimeType:      mimeType,
129	})
130	if err != nil {
131		writeError(w, http.StatusInternalServerError, "failed to save metadata")
132		return
133	}
134
135	// Set tags.
136	if len(tagList) > 0 {
137		if err := a.DB.SetImageTags(r.Context(), img.ID, tagList); err != nil {
138			writeError(w, http.StatusInternalServerError, "failed to save tags")
139			return
140		}
141		img.Tags = tagList
142	}
143
144	writeJSON(w, http.StatusCreated, img)
145}