前言

在之前的项目中,有做过文件上传下载操作,当时对这些操作都不是非常熟悉,所以在实现功能上,停留在能正常工作的前提下,在找到更好的方法后,回头来尝试优化之前的解决方法。

上传操作中,服务器常使用ParseMultipartForm,解析form表单传递的文件数据。

问题

1
2
3
4
5
// in the body of a http.HandlerFunc
err := r.ParseMultipartForm(32 << 20) // 32Mb
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}

调用函数ParseMultipartForm是没有问题的,它会解析http的request body。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ParseMultipartForm parses a request body as multipart/form-data.
// The whole request body is parsed and up to a total of maxMemory bytes of
// its file parts are stored in memory, with the remainder stored on
// disk in temporary files.
// ParseMultipartForm calls ParseForm if necessary.
// After one call to ParseMultipartForm, subsequent calls have no effect.

// ParseMultipartForm将请求主体解析为multipart/form-data。
// 解析整个请求体,并将其文件的总maxMemory字节存储在内存中,其余部分存储在
// 临时文件中的磁盘。
func (r *Request) ParseMultipartForm(maxMemory int64) error {
...
}

根据注释,可以得知,不管文件是否过多,或者文件是否为serve端需要,这些文件都会被缓存,这里体现出这个函数对上传的操作性不是太强,若想要增强对文件上传一些限制操作,也就是在读取文件缓存前的一些限制。

  • 文件类型验证
  • 文件大小验证
  • 白名单“字段”
  • 按顺序解析字段,如果没有则终止
  • 如果任何验证失败,则提前终止
  • 没有预先分配
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
// function body of a http.HandlerFunc
r.Body = http.MaxBytesReader(w, r.Body, 32<<20+1024)
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// parse text field
text := make([]byte, 512)
p, err := reader.NextPart()
// one more field to parse, EOF is considered as failure here
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if p.FormName() != "text_field" {
http.Error(w, "text_field is expected", http.StatusBadRequest)
return
}
_, err = p.Read(text)
if err != nil && err != io.EOF {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// parse file field
p, err = reader.NextPart()
if err != nil && err != io.EOF {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if p.FormName() != "file_field" {
http.Error(w, "file_field is expected", http.StatusBadRequest)
return
}
buf := bufio.NewReader(p)
sniff, _ := buf.Peek(512) // 判断文件类型
contentType := http.DetectContentType(sniff)
if contentType != "application/zip" {
http.Error(w, "file type not allowed", http.StatusBadRequest)
return
}
f, err := ioutil.TempFile("", "")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
var maxSize int64 = 32 << 20
lmt := io.MultiReader(buf, io.LimitReader(p, maxSize - 511))
written, err := io.Copy(f, lmt)
if err != nil && err != io.EOF {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if written > maxSize {
os.Remove(f.Name())
http.Error(w, "file size over limit", http.StatusBadRequest)
return
}
// schedule for other stuffs (s3, scanning, etc.)

内容参考自:https://medium.com/@owlwalks/dont-parse-everything-from-client-multipart-post-golang-9280d23cd4ad