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.
107 tagStr := r.FormValue("tags")
108 var tagList []string
109 if tagStr != "" {
110 for _, t := range strings.Split(tagStr, ",") {
111 t = strings.TrimSpace(t)
112 if t != "" {
113 tagList = append(tagList, t)
114 }
115 }
116 }
117
118 // Create DB record.
119 img, err := a.DB.CreateImage(r.Context(), &db.Image{
120 Filename: header.Filename,
121 StoragePath: storagePath,
122 ThumbnailPath: thumbnailPath,
123 UploadedBy: user.ID,
124 FileSize: int64(len(data)),
125 Width: width,
126 Height: height,
127 MimeType: mimeType,
128 })
129 if err != nil {
130 writeError(w, http.StatusInternalServerError, "failed to save metadata")
131 return
132 }
133
134 // Set tags.
135 if len(tagList) > 0 {
136 if err := a.DB.SetImageTags(r.Context(), img.ID, tagList); err != nil {
137 writeError(w, http.StatusInternalServerError, "failed to save tags")
138 return
139 }
140 img.Tags = tagList
141 }
142
143 writeJSON(w, http.StatusCreated, img)
144}