Skip to content

Commit d1ab8e8

Browse files
martinyonatannaldas
authored andcommitted
bind: add support of multipart multi files
1 parent ab87b63 commit d1ab8e8

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

bind.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/xml"
99
"errors"
1010
"fmt"
11+
"mime/multipart"
1112
"net/http"
1213
"reflect"
1314
"strconv"
@@ -90,14 +91,22 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
9091
}
9192
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
9293
}
93-
case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
94+
case strings.HasPrefix(ctype, MIMEApplicationForm):
9495
params, err := c.FormParams()
9596
if err != nil {
9697
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
9798
}
9899
if err = b.bindData(i, params, "form"); err != nil {
99100
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
100101
}
102+
case strings.HasPrefix(ctype, MIMEMultipartForm):
103+
params, err := c.MultipartForm()
104+
if err != nil {
105+
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
106+
}
107+
if err = b.bindData(i, params.Value, "form", params.File); err != nil {
108+
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
109+
}
101110
default:
102111
return ErrUnsupportedMediaType
103112
}
@@ -132,8 +141,8 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
132141
}
133142

134143
// bindData will bind data ONLY fields in destination struct that have EXPLICIT tag
135-
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string) error {
136-
if destination == nil || len(data) == 0 {
144+
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string, files ...map[string][]*multipart.FileHeader) error {
145+
if destination == nil || (len(data) == 0 && len(files) == 0) {
137146
return nil
138147
}
139148
typ := reflect.TypeOf(destination).Elem()
@@ -209,6 +218,37 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
209218
continue
210219
}
211220

221+
// Handle multiple file uploads ([]*multipart.FileHeader, *multipart.FileHeader, []multipart.FileHeader)
222+
if len(files) > 0 {
223+
for _, fileMap := range files {
224+
fileHeaders, exists := fileMap[inputFieldName]
225+
if exists {
226+
if structField.Type() == reflect.TypeOf([]*multipart.FileHeader(nil)) {
227+
structField.Set(reflect.ValueOf(fileHeaders))
228+
continue
229+
} else if structField.Type() == reflect.TypeOf([]multipart.FileHeader(nil)) {
230+
var headers []multipart.FileHeader
231+
for _, fileHeader := range fileHeaders {
232+
headers = append(headers, *fileHeader)
233+
}
234+
structField.Set(reflect.ValueOf(headers))
235+
continue
236+
} else if structField.Type() == reflect.TypeOf(&multipart.FileHeader{}) {
237+
238+
if len(fileHeaders) > 0 {
239+
structField.Set(reflect.ValueOf(fileHeaders[0]))
240+
}
241+
continue
242+
} else if structField.Type() == reflect.TypeOf(multipart.FileHeader{}) {
243+
if len(fileHeaders) > 0 {
244+
structField.Set(reflect.ValueOf(*fileHeaders[0]))
245+
}
246+
continue
247+
}
248+
}
249+
}
250+
}
251+
212252
inputValue, exists := data[inputFieldName]
213253
if !exists {
214254
// Go json.Unmarshal supports case insensitive binding. However the

bind_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,103 @@ func TestDefaultBinder_BindBody(t *testing.T) {
11021102
}
11031103
}
11041104

1105+
type testFile struct {
1106+
Fieldname string
1107+
Filename string
1108+
Content []byte
1109+
}
1110+
1111+
// createRequestMultipartFiles creates a multipart HTTP request with multiple files.
1112+
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
1113+
var body bytes.Buffer
1114+
mw := multipart.NewWriter(&body)
1115+
1116+
for _, file := range files {
1117+
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
1118+
assert.NoError(t, err)
1119+
1120+
n, err := fw.Write(file.Content)
1121+
assert.NoError(t, err)
1122+
assert.Equal(t, len(file.Content), n)
1123+
}
1124+
1125+
err := mw.Close()
1126+
assert.NoError(t, err)
1127+
1128+
req, err := http.NewRequest(http.MethodPost, "/", &body)
1129+
assert.NoError(t, err)
1130+
1131+
req.Header.Set("Content-Type", mw.FormDataContentType())
1132+
1133+
return req
1134+
}
1135+
1136+
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
1137+
assert.Equal(t, file.Filename, fh.Filename)
1138+
assert.Equal(t, int64(len(file.Content)), fh.Size)
1139+
fl, err := fh.Open()
1140+
assert.NoError(t, err)
1141+
body, err := io.ReadAll(fl)
1142+
assert.NoError(t, err)
1143+
assert.Equal(t, string(file.Content), string(body))
1144+
err = fl.Close()
1145+
assert.NoError(t, err)
1146+
}
1147+
1148+
func TestFormMultipartBindTwoFiles(t *testing.T) {
1149+
var args struct {
1150+
Files []*multipart.FileHeader `form:"files"`
1151+
}
1152+
1153+
files := []testFile{
1154+
{
1155+
Fieldname: "files",
1156+
Filename: "file1.txt",
1157+
Content: []byte("This is the content of file 1."),
1158+
},
1159+
{
1160+
Fieldname: "files",
1161+
Filename: "file2.txt",
1162+
Content: []byte("This is the content of file 2."),
1163+
},
1164+
}
1165+
1166+
e := New()
1167+
req := createRequestMultipartFiles(t, files...)
1168+
rec := httptest.NewRecorder()
1169+
c := e.NewContext(req, rec)
1170+
1171+
err := c.Bind(&args)
1172+
assert.NoError(t, err)
1173+
1174+
assert.Len(t, args.Files, len(files))
1175+
for idx, file := range files {
1176+
assertMultipartFileHeader(t, args.Files[idx], file)
1177+
}
1178+
}
1179+
1180+
func TestFormMultipartBindOneFile(t *testing.T) {
1181+
var args struct {
1182+
File *multipart.FileHeader `form:"file"`
1183+
}
1184+
1185+
file := testFile{
1186+
Fieldname: "file",
1187+
Filename: "file1.txt",
1188+
Content: []byte("This is the content of file 1."),
1189+
}
1190+
1191+
e := New()
1192+
req := createRequestMultipartFiles(t, file)
1193+
rec := httptest.NewRecorder()
1194+
c := e.NewContext(req, rec)
1195+
1196+
err := c.Bind(&args)
1197+
assert.NoError(t, err)
1198+
1199+
assertMultipartFileHeader(t, args.File, file)
1200+
}
1201+
11051202
func testBindURL(queryString string, target any) error {
11061203
e := New()
11071204
req := httptest.NewRequest(http.MethodGet, queryString, nil)

0 commit comments

Comments
 (0)