internal/api/upload.go (view raw)
1package api
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "strings"
8
9 "codeberg.org/maxwelljensen/weimar/internal/db"
10 "codeberg.org/maxwelljensen/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 // Compute innate tags based on media characteristics.
120 videoPath := ""
121 if storage.IsVideo(mimeType) {
122 videoPath = a.Storage.AbsPath(storagePath)
123 }
124 innateTags := storage.ComputeInnateTags(mimeType, width, height, videoPath)
125
126 // Create DB record.
127 img, err := a.DB.CreateImage(r.Context(), &db.Image{
128 Filename: header.Filename,
129 StoragePath: storagePath,
130 ThumbnailPath: thumbnailPath,
131 UploadedBy: user.ID,
132 FileSize: int64(len(data)),
133 Width: width,
134 Height: height,
135 MimeType: mimeType,
136 InnateTags: innateTags,
137 })
138 if err != nil {
139 writeError(w, http.StatusInternalServerError, "failed to save metadata")
140 return
141 }
142
143 // Set tags.
144 if len(tagList) > 0 {
145 if err := a.DB.SetImageTags(r.Context(), img.ID, tagList); err != nil {
146 writeError(w, http.StatusInternalServerError, "failed to save tags")
147 return
148 }
149 img.Tags = tagList
150 }
151
152 writeJSON(w, http.StatusCreated, img)
153}