forked from bakape/shamichan
/
upload.go
252 lines (227 loc) · 5.8 KB
/
upload.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// Package imager handles image, video, etc. upload requests and processing
package imager
import (
"crypto/md5"
"crypto/sha1"
"database/sql"
"encoding/base64"
"encoding/hex"
"errors"
"io/ioutil"
"log"
"meguca/auth"
"meguca/common"
"meguca/config"
"meguca/db"
"net/http"
"strconv"
"time"
"github.com/Soreil/apngdetector"
"github.com/bakape/thumbnailer"
)
var (
// Map of MIME types to the constants used internally
mimeTypes = map[string]uint8{
"image/jpeg": common.JPEG,
"image/png": common.PNG,
"image/gif": common.GIF,
"application/pdf": common.PDF,
"video/webm": common.WEBM,
"application/ogg": common.OGG,
"video/mp4": common.MP4,
"audio/mpeg": common.MP3,
mime7Zip: common.SevenZip,
mimeTarGZ: common.TGZ,
mimeTarXZ: common.TXZ,
mimeZip: common.ZIP,
"audio/x-flac": common.FLAC,
mimeText: common.TXT,
}
// MIME types from thumbnailer to accept
allowedMimeTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"application/pdf": true,
"video/webm": true,
"application/ogg": true,
"video/mp4": true,
"audio/mpeg": true,
mimeZip: true,
mime7Zip: true,
mimeTarGZ: true,
mimeTarXZ: true,
"audio/x-flac": true,
mimeText: true,
}
errTooLarge = errors.New("file too large")
isTest bool
)
// NewImageUpload handles the clients' image (or other file) upload request
func NewImageUpload(w http.ResponseWriter, r *http.Request) {
// Limit data received to the maximum uploaded file size limit
r.Body = http.MaxBytesReader(w, r.Body, int64(config.Get().MaxSize<<20))
code, id, err := ParseUpload(r)
if err != nil {
LogError(w, r, code, err)
}
w.Write([]byte(id))
}
// UploadImageHash attempts to skip image upload, if the file has already
// been thumbnailed and is stored on the server. The client sends and SHA1 hash
// of the file it wants to upload. The server looks up, if such a file is
// thumbnailed. If yes, generates and sends a new image allocation token to
// the client.
func UploadImageHash(w http.ResponseWriter, req *http.Request) {
buf, err := ioutil.ReadAll(http.MaxBytesReader(w, req.Body, 40))
if err != nil {
LogError(w, req, 500, err)
return
}
hash := string(buf)
_, err = db.GetImage(hash)
switch err {
case nil:
case sql.ErrNoRows:
return
default:
LogError(w, req, 500, err)
return
}
token, err := db.NewImageToken(hash)
if err != nil {
LogError(w, req, 500, err)
}
w.Write([]byte(token))
}
// LogError send the client file upload errors and logs them server-side
func LogError(w http.ResponseWriter, r *http.Request, code int, err error) {
text := err.Error()
http.Error(w, text, code)
if !isTest {
ip, err := auth.GetIP(r)
if err != nil {
ip = "invalid IP"
}
log.Printf("upload error: %s: %s\n", ip, text)
}
}
// ParseUpload parses the upload form. Separate function for cleaner error
// handling and reusability. Returns the HTTP status code of the response and an
// error, if any.
func ParseUpload(req *http.Request) (int, string, error) {
if err := parseUploadForm(req); err != nil {
return 400, "", err
}
file, _, err := req.FormFile("image")
if err != nil {
return 400, "", err
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
return 500, "", err
}
sum := sha1.Sum(data)
SHA1 := hex.EncodeToString(sum[:])
img, err := db.GetImage(SHA1)
switch err {
case nil: // Already have a thumbnail
return newImageToken(SHA1)
case sql.ErrNoRows:
img.SHA1 = SHA1
return newThumbnail(data, img)
default:
return 500, "", err
}
}
func newImageToken(SHA1 string) (int, string, error) {
token, err := db.NewImageToken(SHA1)
code := 200
if err != nil {
code = 500
}
return code, token, err
}
// Parse and validate the form of the upload request
func parseUploadForm(req *http.Request) error {
length, err := strconv.ParseUint(req.Header.Get("Content-Length"), 10, 64)
if err != nil {
return err
}
if length > uint64(config.Get().MaxSize<<20) {
return errTooLarge
}
return req.ParseMultipartForm(0)
}
// Create a new thumbnail, commit its resources to the DB and filesystem, and
// pass the image data to the client.
func newThumbnail(data []byte, img common.ImageCommon) (int, string, error) {
conf := config.Get()
thumb, err := processFile(data, &img, thumbnailer.Options{
JPEGQuality: conf.JPEGQuality,
MaxSourceDims: thumbnailer.Dims{
Width: uint(conf.MaxWidth),
Height: uint(conf.MaxHeight),
},
ThumbDims: thumbnailer.Dims{
Width: 150,
Height: 150,
},
AcceptedMimeTypes: allowedMimeTypes,
})
switch err.(type) {
case nil:
case thumbnailer.UnsupportedMIMEError:
return 400, "", err
default:
return 500, "", err
}
if err := db.AllocateImage(data, thumb, img); err != nil {
return 500, "", err
}
return newImageToken(img.SHA1)
}
// Separate function for easier testability
func processFile(
data []byte,
img *common.ImageCommon,
opts thumbnailer.Options,
) (
thumbData []byte,
err error,
) {
src, thumb, err := thumbnailer.ProcessBuffer(data, opts)
switch err {
case nil:
case thumbnailer.ErrNoCoverArt:
err = nil
default:
return
}
thumbData = thumb.Data
img.FileType = mimeTypes[src.Mime]
if img.FileType == common.PNG {
img.APNG = apngdetector.Detect(data)
}
if thumb.Data == nil {
img.ThumbType = common.NoFile
} else if thumb.IsPNG {
img.ThumbType = common.PNG
}
img.Audio = src.HasAudio
img.Video = src.HasVideo
img.Length = uint32(src.Length / time.Second)
img.Size = len(data)
img.Artist = src.Artist
img.Title = src.Title
img.Dims = [4]uint16{
uint16(src.Width),
uint16(src.Height),
uint16(thumb.Width),
uint16(thumb.Height),
}
sum := md5.Sum(data)
img.MD5 = base64.RawURLEncoding.EncodeToString(sum[:])
return
}