1
0
mirror of https://github.com/labstack/echo.git synced 2025-03-11 14:49:56 +02:00

bind: add support of multipart multi files

This commit is contained in:
martinpasaribu 2024-10-06 21:42:24 +07:00 committed by Martti T.
parent ab87b63640
commit d1ab8e8544
2 changed files with 140 additions and 3 deletions

46
bind.go
View File

@ -8,6 +8,7 @@ import (
"encoding/xml"
"errors"
"fmt"
"mime/multipart"
"net/http"
"reflect"
"strconv"
@ -90,7 +91,7 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
}
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
case strings.HasPrefix(ctype, MIMEApplicationForm):
params, err := c.FormParams()
if err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
@ -98,6 +99,14 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
if err = b.bindData(i, params, "form"); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
case strings.HasPrefix(ctype, MIMEMultipartForm):
params, err := c.MultipartForm()
if err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
if err = b.bindData(i, params.Value, "form", params.File); err != nil {
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
default:
return ErrUnsupportedMediaType
}
@ -132,8 +141,8 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
}
// bindData will bind data ONLY fields in destination struct that have EXPLICIT tag
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string) error {
if destination == nil || len(data) == 0 {
func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string, files ...map[string][]*multipart.FileHeader) error {
if destination == nil || (len(data) == 0 && len(files) == 0) {
return nil
}
typ := reflect.TypeOf(destination).Elem()
@ -209,6 +218,37 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
continue
}
// Handle multiple file uploads ([]*multipart.FileHeader, *multipart.FileHeader, []multipart.FileHeader)
if len(files) > 0 {
for _, fileMap := range files {
fileHeaders, exists := fileMap[inputFieldName]
if exists {
if structField.Type() == reflect.TypeOf([]*multipart.FileHeader(nil)) {
structField.Set(reflect.ValueOf(fileHeaders))
continue
} else if structField.Type() == reflect.TypeOf([]multipart.FileHeader(nil)) {
var headers []multipart.FileHeader
for _, fileHeader := range fileHeaders {
headers = append(headers, *fileHeader)
}
structField.Set(reflect.ValueOf(headers))
continue
} else if structField.Type() == reflect.TypeOf(&multipart.FileHeader{}) {
if len(fileHeaders) > 0 {
structField.Set(reflect.ValueOf(fileHeaders[0]))
}
continue
} else if structField.Type() == reflect.TypeOf(multipart.FileHeader{}) {
if len(fileHeaders) > 0 {
structField.Set(reflect.ValueOf(*fileHeaders[0]))
}
continue
}
}
}
}
inputValue, exists := data[inputFieldName]
if !exists {
// Go json.Unmarshal supports case insensitive binding. However the

View File

@ -1102,6 +1102,103 @@ func TestDefaultBinder_BindBody(t *testing.T) {
}
}
type testFile struct {
Fieldname string
Filename string
Content []byte
}
// createRequestMultipartFiles creates a multipart HTTP request with multiple files.
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(http.MethodPost, "/", &body)
assert.NoError(t, err)
req.Header.Set("Content-Type", mw.FormDataContentType())
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)
fl, err := fh.Open()
assert.NoError(t, err)
body, err := io.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
}
func TestFormMultipartBindTwoFiles(t *testing.T) {
var args struct {
Files []*multipart.FileHeader `form:"files"`
}
files := []testFile{
{
Fieldname: "files",
Filename: "file1.txt",
Content: []byte("This is the content of file 1."),
},
{
Fieldname: "files",
Filename: "file2.txt",
Content: []byte("This is the content of file 2."),
},
}
e := New()
req := createRequestMultipartFiles(t, files...)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := c.Bind(&args)
assert.NoError(t, err)
assert.Len(t, args.Files, len(files))
for idx, file := range files {
assertMultipartFileHeader(t, args.Files[idx], file)
}
}
func TestFormMultipartBindOneFile(t *testing.T) {
var args struct {
File *multipart.FileHeader `form:"file"`
}
file := testFile{
Fieldname: "file",
Filename: "file1.txt",
Content: []byte("This is the content of file 1."),
}
e := New()
req := createRequestMultipartFiles(t, file)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := c.Bind(&args)
assert.NoError(t, err)
assertMultipartFileHeader(t, args.File, file)
}
func testBindURL(queryString string, target any) error {
e := New()
req := httptest.NewRequest(http.MethodGet, queryString, nil)