@@ -10,12 +10,11 @@ import (
1010 "io"
1111 "net/http"
1212 "os"
13- "path/filepath"
1413)
1514
1615const (
17- maxUploadBytes = 8 << 20 // 8 MiB — generous for a phone photo.
18- sniffLen = 512 // bytes http.DetectContentType needs.
16+ maxUploadBytes = 15 << 20 // 15 MiB — generous for a full-res phone photo.
17+ sniffLen = 512 // bytes http.DetectContentType needs.
1918)
2019
2120// extByType maps the sniffed content type to a safe, server-chosen extension.
@@ -29,14 +28,14 @@ var extByType = map[string]string{
2928}
3029
3130// Handler returns an http.HandlerFunc that accepts a multipart "file" field,
32- // validates it is an allowed image, stores it under dir with a random name,
33- // and responds {"url":"/uploads/<name >"}. Mount it behind auth.
34- func Handler (dir string ) http.HandlerFunc {
31+ // validates it is an allowed image, hands it to the Store with a random name,
32+ // and responds {"url":"<public url >"}. Mount it behind auth.
33+ func Handler (store Store ) http.HandlerFunc {
3534 return func (w http.ResponseWriter , r * http.Request ) {
3635 // Cap the whole request body before reading anything.
3736 r .Body = http .MaxBytesReader (w , r .Body , maxUploadBytes + 1024 )
3837 if err := r .ParseMultipartForm (maxUploadBytes + 1024 ); err != nil {
39- http .Error (w , "file too large or malformed (max 8MB )" , http .StatusRequestEntityTooLarge )
38+ http .Error (w , "file too large or malformed (max 15MB )" , http .StatusRequestEntityTooLarge )
4039 return
4140 }
4241 file , _ , err := r .FormFile ("file" )
@@ -46,11 +45,23 @@ func Handler(dir string) http.HandlerFunc {
4645 }
4746 defer file .Close ()
4847
48+ data , err := io .ReadAll (io .LimitReader (file , maxUploadBytes + 1 ))
49+ if err != nil {
50+ http .Error (w , "read failed" , http .StatusInternalServerError )
51+ return
52+ }
53+ if len (data ) > maxUploadBytes {
54+ http .Error (w , "image too large (max 15MB)" , http .StatusRequestEntityTooLarge )
55+ return
56+ }
57+
4958 // Sniff the real content type from the bytes, not the client's claim.
50- head := make ([]byte , sniffLen )
51- n , _ := io .ReadFull (file , head )
52- head = head [:n ]
53- ext , ok := extByType [http .DetectContentType (head )]
59+ sniff := data
60+ if len (sniff ) > sniffLen {
61+ sniff = sniff [:sniffLen ]
62+ }
63+ contentType := http .DetectContentType (sniff )
64+ ext , ok := extByType [contentType ]
5465 if ! ok {
5566 http .Error (w , "only JPEG, PNG, WebP or GIF images are allowed" , http .StatusUnsupportedMediaType )
5667 return
@@ -63,29 +74,14 @@ func Handler(dir string) http.HandlerFunc {
6374 }
6475 name += ext
6576
66- if err := os .MkdirAll (dir , 0o755 ); err != nil {
67- http .Error (w , "storage unavailable" , http .StatusInternalServerError )
68- return
69- }
70- dst , err := os .Create (filepath .Join (dir , name ))
77+ url , err := store .Put (r .Context (), name , contentType , data )
7178 if err != nil {
72- http .Error (w , "could not store file" , http .StatusInternalServerError )
73- return
74- }
75- defer dst .Close ()
76-
77- // Write the sniffed head, then the rest, bounded again as defence in depth.
78- if _ , err := dst .Write (head ); err != nil {
79- http .Error (w , "write failed" , http .StatusInternalServerError )
80- return
81- }
82- if _ , err := io .Copy (dst , io .LimitReader (file , maxUploadBytes )); err != nil {
83- http .Error (w , "write failed" , http .StatusInternalServerError )
79+ http .Error (w , "could not store image" , http .StatusInternalServerError )
8480 return
8581 }
8682
8783 w .Header ().Set ("Content-Type" , "application/json" )
88- _ = json .NewEncoder (w ).Encode (map [string ]string {"url" : "/uploads/" + name })
84+ _ = json .NewEncoder (w ).Encode (map [string ]string {"url" : url })
8985 }
9086}
9187
0 commit comments