From d1ab8e8544c30bda058a98878c8bd36d5aba7636 Mon Sep 17 00:00:00 2001 From: martinpasaribu Date: Sun, 6 Oct 2024 21:42:24 +0700 Subject: [PATCH] bind: add support of multipart multi files --- bind.go | 46 +++++++++++++++++++++++-- bind_test.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/bind.go b/bind.go index 877c0f5c..a127f99f 100644 --- a/bind.go +++ b/bind.go @@ -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 diff --git a/bind_test.go b/bind_test.go index fc0c0059..f52c1319 100644 --- a/bind_test.go +++ b/bind_test.go @@ -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)