From 99bf8e8630234da4b4a05442b76683df7dd4dd32 Mon Sep 17 00:00:00 2001 From: Karl Haas Date: Wed, 2 Aug 2023 14:08:51 +0000 Subject: [PATCH] feature: Support the upload of multiple files --- binding/file_request_type_binder.go | 29 ++++++++---- binding/file_test.go | 68 +++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/binding/file_request_type_binder.go b/binding/file_request_type_binder.go index 0d99c9e7b..570f9ba73 100644 --- a/binding/file_request_type_binder.go +++ b/binding/file_request_type_binder.go @@ -52,17 +52,30 @@ func (ht FileRequestTypeBinder) BinderFunc() Binder { if !f.IsValid() { continue } - if _, ok := f.Interface().(File); !ok { + if f.Kind() == reflect.Slice { + for _, fh := range req.MultipartForm.File[n] { + mf, err := fh.Open() + if err != nil { + return err + } + + f.Set(reflect.Append(f, reflect.ValueOf(File{ + File: mf, + FileHeader: fh, + }))) + } continue } - mf, mh, err := req.FormFile(n) - if err != nil { - return err + if _, ok := f.Interface().(File); ok { + mf, mh, err := req.FormFile(n) + if err != nil { + return err + } + f.Set(reflect.ValueOf(File{ + File: mf, + FileHeader: mh, + })) } - f.Set(reflect.ValueOf(File{ - File: mf, - FileHeader: mh, - })) } return nil diff --git a/binding/file_test.go b/binding/file_test.go index ca66a5294..4ed01792f 100644 --- a/binding/file_test.go +++ b/binding/file_test.go @@ -2,12 +2,14 @@ package binding_test import ( "bytes" + "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" + "strings" "testing" "github.com/gobuffalo/buffalo" @@ -20,6 +22,10 @@ type WithFile struct { MyFile binding.File } +type NamedFileSlice struct { + MyFiles []binding.File `form:"thefiles"` +} + type NamedFile struct { MyFile binding.File `form:"afile"` } @@ -40,6 +46,18 @@ func App() *buffalo.App { } return c.Render(http.StatusCreated, render.String(wf.MyFile.Filename)) }) + a.POST("/named-file-slice", func(c buffalo.Context) error { + wmf := &NamedFileSlice{} + if err := c.Bind(wmf); err != nil { + return err + } + result := make([]string, len(wmf.MyFiles)) + for i, f := range wmf.MyFiles { + result[i] += fmt.Sprintf("%s:%d", f.Filename, f.Size) + + } + return c.Render(http.StatusCreated, render.String(strings.Join(result, ","))) + }) a.POST("/on-context", func(c buffalo.Context) error { f, err := c.File("MyFile") if err != nil { @@ -54,7 +72,7 @@ func App() *buffalo.App { func Test_File_Upload_On_Struct(t *testing.T) { r := require.New(t) - req, err := newfileUploadRequest("/on-struct", "MyFile", "file_test.go") + req, err := newFileUploadRequest("/on-struct", "MyFile", "file_test.go") r.NoError(err) res := httptest.NewRecorder() @@ -64,10 +82,23 @@ func Test_File_Upload_On_Struct(t *testing.T) { r.Equal("file_test.go", res.Body.String()) } +func Test_File_Upload_On_Struct_WithTag_WithMultipleFiles(t *testing.T) { + r := require.New(t) + + req, err := newFileUploadRequest("/named-file-slice", "thefiles", "file_test.go", "file.go", "types.go") + r.NoError(err) + res := httptest.NewRecorder() + + App().ServeHTTP(res, req) + + r.Equal(http.StatusCreated, res.Code) + r.Equal("file_test.go:3672,file.go:348,types.go:507", res.Body.String()) +} + func Test_File_Upload_On_Struct_WithTag(t *testing.T) { r := require.New(t) - req, err := newfileUploadRequest("/named-file", "afile", "file_test.go") + req, err := newFileUploadRequest("/named-file", "afile", "file_test.go") r.NoError(err) res := httptest.NewRecorder() @@ -80,7 +111,7 @@ func Test_File_Upload_On_Struct_WithTag(t *testing.T) { func Test_File_Upload_On_Context(t *testing.T) { r := require.New(t) - req, err := newfileUploadRequest("/on-context", "MyFile", "file_test.go") + req, err := newFileUploadRequest("/on-context", "MyFile", "file_test.go") r.NoError(err) res := httptest.NewRecorder() @@ -92,27 +123,28 @@ func Test_File_Upload_On_Context(t *testing.T) { // this helper method was inspired by this blog post by Matt Aimonetti: // https://matt.aimonetti.net/posts/2013/07/01/golang-multipart-file-upload-example/ -func newfileUploadRequest(uri string, paramName, path string) (*http.Request, error) { - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - +func newFileUploadRequest(uri string, paramName string, paths ...string) (*http.Request, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile(paramName, filepath.Base(path)) - if err != nil { - return nil, err - } - if _, err = io.Copy(part, file); err != nil { - return nil, err + + for _, path := range paths { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + part, err := writer.CreateFormFile(paramName, filepath.Base(path)) + if err != nil { + return nil, err + } + if _, err = io.Copy(part, file); err != nil { + return nil, err + } } - if err = writer.Close(); err != nil { + if err := writer.Close(); err != nil { return nil, err } - req, err := http.NewRequest("POST", uri, body) req.Header.Set("Content-Type", writer.FormDataContentType()) return req, err