-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
binding: add support of multipart multi files (#1878)
- Loading branch information
Showing
3 changed files
with
204 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright 2019 Gin Core Team. All rights reserved. | ||
// Use of this source code is governed by a MIT style | ||
// license that can be found in the LICENSE file. | ||
|
||
package binding | ||
|
||
import ( | ||
"errors" | ||
"mime/multipart" | ||
"net/http" | ||
"reflect" | ||
) | ||
|
||
type multipartRequest http.Request | ||
|
||
var _ setter = (*multipartRequest)(nil) | ||
|
||
// TrySet tries to set a value by the multipart request with the binding a form file | ||
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { | ||
if files := r.MultipartForm.File[key]; len(files) != 0 { | ||
return setByMultipartFormFile(value, field, files) | ||
} | ||
|
||
return setByForm(value, field, r.MultipartForm.Value, key, opt) | ||
} | ||
|
||
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { | ||
switch value.Kind() { | ||
case reflect.Ptr: | ||
switch value.Interface().(type) { | ||
case *multipart.FileHeader: | ||
value.Set(reflect.ValueOf(files[0])) | ||
return true, nil | ||
} | ||
case reflect.Struct: | ||
switch value.Interface().(type) { | ||
case multipart.FileHeader: | ||
value.Set(reflect.ValueOf(*files[0])) | ||
return true, nil | ||
} | ||
case reflect.Slice: | ||
slice := reflect.MakeSlice(value.Type(), len(files), len(files)) | ||
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files) | ||
if err != nil || !isSetted { | ||
return isSetted, err | ||
} | ||
value.Set(slice) | ||
return true, nil | ||
case reflect.Array: | ||
return setArrayOfMultipartFormFiles(value, field, files) | ||
} | ||
return false, errors.New("unsupported field type for multipart.FileHeader") | ||
} | ||
|
||
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { | ||
if value.Len() != len(files) { | ||
return false, errors.New("unsupported len of array for []*multipart.FileHeader") | ||
} | ||
for i := range files { | ||
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1]) | ||
if err != nil || !setted { | ||
return setted, err | ||
} | ||
} | ||
return true, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright 2019 Gin Core Team. All rights reserved. | ||
// Use of this source code is governed by a MIT style | ||
// license that can be found in the LICENSE file. | ||
|
||
package binding | ||
|
||
import ( | ||
"bytes" | ||
"io/ioutil" | ||
"mime/multipart" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestFormMultipartBindingBindOneFile(t *testing.T) { | ||
var s struct { | ||
FileValue multipart.FileHeader `form:"file"` | ||
FilePtr *multipart.FileHeader `form:"file"` | ||
SliceValues []multipart.FileHeader `form:"file"` | ||
SlicePtrs []*multipart.FileHeader `form:"file"` | ||
ArrayValues [1]multipart.FileHeader `form:"file"` | ||
ArrayPtrs [1]*multipart.FileHeader `form:"file"` | ||
} | ||
file := testFile{"file", "file1", []byte("hello")} | ||
|
||
req := createRequestMultipartFiles(t, file) | ||
err := FormMultipart.Bind(req, &s) | ||
assert.NoError(t, err) | ||
|
||
assertMultipartFileHeader(t, &s.FileValue, file) | ||
assertMultipartFileHeader(t, s.FilePtr, file) | ||
assert.Len(t, s.SliceValues, 1) | ||
assertMultipartFileHeader(t, &s.SliceValues[0], file) | ||
assert.Len(t, s.SlicePtrs, 1) | ||
assertMultipartFileHeader(t, s.SlicePtrs[0], file) | ||
assertMultipartFileHeader(t, &s.ArrayValues[0], file) | ||
assertMultipartFileHeader(t, s.ArrayPtrs[0], file) | ||
} | ||
|
||
func TestFormMultipartBindingBindTwoFiles(t *testing.T) { | ||
var s struct { | ||
SliceValues []multipart.FileHeader `form:"file"` | ||
SlicePtrs []*multipart.FileHeader `form:"file"` | ||
ArrayValues [2]multipart.FileHeader `form:"file"` | ||
ArrayPtrs [2]*multipart.FileHeader `form:"file"` | ||
} | ||
files := []testFile{ | ||
{"file", "file1", []byte("hello")}, | ||
{"file", "file2", []byte("world")}, | ||
} | ||
|
||
req := createRequestMultipartFiles(t, files...) | ||
err := FormMultipart.Bind(req, &s) | ||
assert.NoError(t, err) | ||
|
||
assert.Len(t, s.SliceValues, len(files)) | ||
assert.Len(t, s.SlicePtrs, len(files)) | ||
assert.Len(t, s.ArrayValues, len(files)) | ||
assert.Len(t, s.ArrayPtrs, len(files)) | ||
|
||
for i, file := range files { | ||
assertMultipartFileHeader(t, &s.SliceValues[i], file) | ||
assertMultipartFileHeader(t, s.SlicePtrs[i], file) | ||
assertMultipartFileHeader(t, &s.ArrayValues[i], file) | ||
assertMultipartFileHeader(t, s.ArrayPtrs[i], file) | ||
} | ||
} | ||
|
||
func TestFormMultipartBindingBindError(t *testing.T) { | ||
files := []testFile{ | ||
{"file", "file1", []byte("hello")}, | ||
{"file", "file2", []byte("world")}, | ||
} | ||
|
||
for _, tt := range []struct { | ||
name string | ||
s interface{} | ||
}{ | ||
{"wrong type", &struct { | ||
Files int `form:"file"` | ||
}{}}, | ||
{"wrong array size", &struct { | ||
Files [1]*multipart.FileHeader `form:"file"` | ||
}{}}, | ||
{"wrong slice type", &struct { | ||
Files []int `form:"file"` | ||
}{}}, | ||
} { | ||
req := createRequestMultipartFiles(t, files...) | ||
err := FormMultipart.Bind(req, tt.s) | ||
assert.Error(t, err) | ||
} | ||
} | ||
|
||
type testFile struct { | ||
Fieldname string | ||
Filename string | ||
Content []byte | ||
} | ||
|
||
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request { | ||
var body bytes.Buffer | ||
|
||
mw := multipart.NewWriter(&body) | ||
for _, file := range files { | ||
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename) | ||
assert.NoError(t, err) | ||
|
||
n, err := fw.Write(file.Content) | ||
assert.NoError(t, err) | ||
assert.Equal(t, len(file.Content), n) | ||
} | ||
err := mw.Close() | ||
assert.NoError(t, err) | ||
|
||
req, err := http.NewRequest("POST", "/", &body) | ||
assert.NoError(t, err) | ||
|
||
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary()) | ||
return req | ||
} | ||
|
||
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) { | ||
assert.Equal(t, file.Filename, fh.Filename) | ||
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8 | ||
|
||
fl, err := fh.Open() | ||
assert.NoError(t, err) | ||
|
||
body, err := ioutil.ReadAll(fl) | ||
assert.NoError(t, err) | ||
assert.Equal(t, string(file.Content), string(body)) | ||
|
||
err = fl.Close() | ||
assert.NoError(t, err) | ||
} |