Skip to content

Commit 673f3fc

Browse files
vkdThomasObenaus
authored andcommitted
binding: add support of multipart multi files (gin-gonic#1878) (gin-gonic#1949)
* binding: add support of multipart multi files (gin-gonic#1878) * update readme: add multipart file binding
1 parent 93dd49a commit 673f3fc

File tree

4 files changed

+225
-43
lines changed

4 files changed

+225
-43
lines changed

README.md

+21-17
Original file line numberDiff line numberDiff line change
@@ -959,40 +959,44 @@ result:
959959
### Multipart/Urlencoded binding
960960

961961
```go
962-
package main
962+
type ProfileForm struct {
963+
Name string `form:"name" binding:"required"`
964+
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
963965

964-
import (
965-
"github.com/gin-gonic/gin"
966-
)
967-
968-
type LoginForm struct {
969-
User string `form:"user" binding:"required"`
970-
Password string `form:"password" binding:"required"`
966+
// or for multiple files
967+
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
971968
}
972969

973970
func main() {
974971
router := gin.Default()
975-
router.POST("/login", func(c *gin.Context) {
972+
router.POST("/profile", func(c *gin.Context) {
976973
// you can bind multipart form with explicit binding declaration:
977974
// c.ShouldBindWith(&form, binding.Form)
978975
// or you can simply use autobinding with ShouldBind method:
979-
var form LoginForm
976+
var form ProfileForm
980977
// in this case proper binding will be automatically selected
981-
if c.ShouldBind(&form) == nil {
982-
if form.User == "user" && form.Password == "password" {
983-
c.JSON(200, gin.H{"status": "you are logged in"})
984-
} else {
985-
c.JSON(401, gin.H{"status": "unauthorized"})
986-
}
978+
if err := c.ShouldBind(&form); err != nil {
979+
c.String(http.StatusBadRequest, "bad request")
980+
return
987981
}
982+
983+
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
984+
if err != nil {
985+
c.String(http.StatusInternalServerError, "unknown error")
986+
return
987+
}
988+
989+
// db.Save(&form)
990+
991+
c.String(http.StatusOK, "ok")
988992
})
989993
router.Run(":8080")
990994
}
991995
```
992996

993997
Test it with:
994998
```sh
995-
$ curl -v --form user=user --form password=password http://localhost:8080/login
999+
$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
9961000
```
9971001

9981002
### XML, JSON, YAML and ProtoBuf rendering

binding/form.go

-26
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
package binding
66

77
import (
8-
"mime/multipart"
98
"net/http"
10-
"reflect"
119
)
1210

1311
const defaultMemory = 32 * 1024 * 1024
@@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
6361

6462
return validate(obj)
6563
}
66-
67-
type multipartRequest http.Request
68-
69-
var _ setter = (*multipartRequest)(nil)
70-
71-
var (
72-
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
73-
)
74-
75-
// TrySet tries to set a value by the multipart request with the binding a form file
76-
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
77-
if value.Type() == multipartFileHeaderStructType {
78-
_, file, err := (*http.Request)(r).FormFile(key)
79-
if err != nil {
80-
return false, err
81-
}
82-
if file != nil {
83-
value.Set(reflect.ValueOf(*file))
84-
return true, nil
85-
}
86-
}
87-
88-
return setByForm(value, field, r.MultipartForm.Value, key, opt)
89-
}

binding/multipart_form_mapping.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2019 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package binding
6+
7+
import (
8+
"errors"
9+
"mime/multipart"
10+
"net/http"
11+
"reflect"
12+
)
13+
14+
type multipartRequest http.Request
15+
16+
var _ setter = (*multipartRequest)(nil)
17+
18+
// TrySet tries to set a value by the multipart request with the binding a form file
19+
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
20+
if files := r.MultipartForm.File[key]; len(files) != 0 {
21+
return setByMultipartFormFile(value, field, files)
22+
}
23+
24+
return setByForm(value, field, r.MultipartForm.Value, key, opt)
25+
}
26+
27+
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
28+
switch value.Kind() {
29+
case reflect.Ptr:
30+
switch value.Interface().(type) {
31+
case *multipart.FileHeader:
32+
value.Set(reflect.ValueOf(files[0]))
33+
return true, nil
34+
}
35+
case reflect.Struct:
36+
switch value.Interface().(type) {
37+
case multipart.FileHeader:
38+
value.Set(reflect.ValueOf(*files[0]))
39+
return true, nil
40+
}
41+
case reflect.Slice:
42+
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
43+
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
44+
if err != nil || !isSetted {
45+
return isSetted, err
46+
}
47+
value.Set(slice)
48+
return true, nil
49+
case reflect.Array:
50+
return setArrayOfMultipartFormFiles(value, field, files)
51+
}
52+
return false, errors.New("unsupported field type for multipart.FileHeader")
53+
}
54+
55+
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
56+
if value.Len() != len(files) {
57+
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
58+
}
59+
for i := range files {
60+
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
61+
if err != nil || !setted {
62+
return setted, err
63+
}
64+
}
65+
return true, nil
66+
}
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2019 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package binding
6+
7+
import (
8+
"bytes"
9+
"io/ioutil"
10+
"mime/multipart"
11+
"net/http"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestFormMultipartBindingBindOneFile(t *testing.T) {
18+
var s struct {
19+
FileValue multipart.FileHeader `form:"file"`
20+
FilePtr *multipart.FileHeader `form:"file"`
21+
SliceValues []multipart.FileHeader `form:"file"`
22+
SlicePtrs []*multipart.FileHeader `form:"file"`
23+
ArrayValues [1]multipart.FileHeader `form:"file"`
24+
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
25+
}
26+
file := testFile{"file", "file1", []byte("hello")}
27+
28+
req := createRequestMultipartFiles(t, file)
29+
err := FormMultipart.Bind(req, &s)
30+
assert.NoError(t, err)
31+
32+
assertMultipartFileHeader(t, &s.FileValue, file)
33+
assertMultipartFileHeader(t, s.FilePtr, file)
34+
assert.Len(t, s.SliceValues, 1)
35+
assertMultipartFileHeader(t, &s.SliceValues[0], file)
36+
assert.Len(t, s.SlicePtrs, 1)
37+
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
38+
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
39+
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
40+
}
41+
42+
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
43+
var s struct {
44+
SliceValues []multipart.FileHeader `form:"file"`
45+
SlicePtrs []*multipart.FileHeader `form:"file"`
46+
ArrayValues [2]multipart.FileHeader `form:"file"`
47+
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
48+
}
49+
files := []testFile{
50+
{"file", "file1", []byte("hello")},
51+
{"file", "file2", []byte("world")},
52+
}
53+
54+
req := createRequestMultipartFiles(t, files...)
55+
err := FormMultipart.Bind(req, &s)
56+
assert.NoError(t, err)
57+
58+
assert.Len(t, s.SliceValues, len(files))
59+
assert.Len(t, s.SlicePtrs, len(files))
60+
assert.Len(t, s.ArrayValues, len(files))
61+
assert.Len(t, s.ArrayPtrs, len(files))
62+
63+
for i, file := range files {
64+
assertMultipartFileHeader(t, &s.SliceValues[i], file)
65+
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
66+
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
67+
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
68+
}
69+
}
70+
71+
func TestFormMultipartBindingBindError(t *testing.T) {
72+
files := []testFile{
73+
{"file", "file1", []byte("hello")},
74+
{"file", "file2", []byte("world")},
75+
}
76+
77+
for _, tt := range []struct {
78+
name string
79+
s interface{}
80+
}{
81+
{"wrong type", &struct {
82+
Files int `form:"file"`
83+
}{}},
84+
{"wrong array size", &struct {
85+
Files [1]*multipart.FileHeader `form:"file"`
86+
}{}},
87+
{"wrong slice type", &struct {
88+
Files []int `form:"file"`
89+
}{}},
90+
} {
91+
req := createRequestMultipartFiles(t, files...)
92+
err := FormMultipart.Bind(req, tt.s)
93+
assert.Error(t, err)
94+
}
95+
}
96+
97+
type testFile struct {
98+
Fieldname string
99+
Filename string
100+
Content []byte
101+
}
102+
103+
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
104+
var body bytes.Buffer
105+
106+
mw := multipart.NewWriter(&body)
107+
for _, file := range files {
108+
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
109+
assert.NoError(t, err)
110+
111+
n, err := fw.Write(file.Content)
112+
assert.NoError(t, err)
113+
assert.Equal(t, len(file.Content), n)
114+
}
115+
err := mw.Close()
116+
assert.NoError(t, err)
117+
118+
req, err := http.NewRequest("POST", "/", &body)
119+
assert.NoError(t, err)
120+
121+
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
122+
return req
123+
}
124+
125+
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
126+
assert.Equal(t, file.Filename, fh.Filename)
127+
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
128+
129+
fl, err := fh.Open()
130+
assert.NoError(t, err)
131+
132+
body, err := ioutil.ReadAll(fl)
133+
assert.NoError(t, err)
134+
assert.Equal(t, string(file.Content), string(body))
135+
136+
err = fl.Close()
137+
assert.NoError(t, err)
138+
}

0 commit comments

Comments
 (0)