Repository: gin-gonic/gin
Pull request #1264 (2018-03-01 14:38:05)

update for supporting file binding
update for supporting multipart form and file binding 

example:
```
type PhoptUploadForm struct {
	imgData    *multipart.FileHeader `form:"img_data" binding:"required"`
	ProjectID   string `form:"project_id" binding:"required"`
	Description string `form:"description binding:"required"`
}
```


ref: https://github.com/gin-gonic/gin/issues/1263

codecov[bot]: # [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=h1) Report
> Merging [#1264](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=desc) into [master](https://codecov.io/gh/gin-gonic/gin/commit/5d3f30cfc81f9109850fa9043a19783cbbde68a5?src=pr&el=desc) will **decrease** coverage by `0.65%`.
> The diff coverage is `50%`.

[![Impacted file tree graph](https://codecov.io/gh/gin-gonic/gin/pull/1264/graphs/tree.svg?width=650&height=150&src=pr&token=Qpqxhwery8)](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1264      +/-   ##
==========================================
- Coverage    98.4%   97.75%   -0.66%     
==========================================
  Files          34       34              
  Lines        1759     1783      +24     
==========================================
+ Hits         1731     1743      +12     
- Misses         23       34      +11     
- Partials        5        6       +1
```


| [Impacted Files](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [binding/form.go](https://codecov.io/gh/gin-gonic/gin/pull/1264/diff?src=pr&el=tree#diff-YmluZGluZy9mb3JtLmdv) | `92.59% <0%> (-7.41%)` | :arrow_down: |
| [binding/binding.go](https://codecov.io/gh/gin-gonic/gin/pull/1264/diff?src=pr&el=tree#diff-YmluZGluZy9iaW5kaW5nLmdv) | `100% <100%> (ø)` | :arrow_up: |
| [binding/form\_mapping.go](https://codecov.io/gh/gin-gonic/gin/pull/1264/diff?src=pr&el=tree#diff-YmluZGluZy9mb3JtX21hcHBpbmcuZ28=) | `91.24% <50%> (-7.05%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=footer). Last update [5d3f30c...345232f](https://codecov.io/gh/gin-gonic/gin/pull/1264?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

thinkerou: @p581581 please fix conflict, thanks!
thinkerou: @p581581 please fix conflict, thanks!
thinkerou: @p581581 any updates? thanks!
p581581: I thought this pr is closed. Give me some time to fix the conflicts.
thinkerou: @p581581 OK, thanks! I will save the pr until you fix it.
haoguo: any progress of this feature? really need it
p581581: Thank you!! @thinkerou 
thinkerou: @appleboy please help review the pull request, thanks!

Patch:
diff --git a/binding/binding.go b/binding/binding.go
index 26d71c9..520c510 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -98,7 +98,9 @@ func Default(method, contentType string) Binding {
 		return MsgPack
 	case MIMEYAML:
 		return YAML
-	default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
+	case MIMEMultipartPOSTForm:
+		return FormMultipart
+	default: // case MIMEPOSTForm:
 		return Form
 	}
 }
diff --git a/binding/form.go b/binding/form.go
index 8955c95..f1f8919 100644
--- a/binding/form.go
+++ b/binding/form.go
@@ -56,5 +56,10 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
 	if err := mapForm(obj, req.MultipartForm.Value); err != nil {
 		return err
 	}
+
+	if err := mapFiles(obj, req); err != nil {
+		return err
+	}
+
 	return validate(obj)
 }
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index ba9d2c4..fc33b1d 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -7,6 +7,7 @@ package binding
 import (
 	"errors"
 	"fmt"
+	"net/http"
 	"reflect"
 	"strconv"
 	"strings"
@@ -15,6 +16,34 @@ import (
 	"github.com/gin-gonic/gin/internal/json"
 )
 
+func mapFiles(ptr interface{}, req *http.Request) error {
+	typ := reflect.TypeOf(ptr).Elem()
+	val := reflect.ValueOf(ptr).Elem()
+	for i := 0; i < typ.NumField(); i++ {
+		typeField := typ.Field(i)
+		structField := val.Field(i)
+
+		t := fmt.Sprintf("%s", typeField.Type)
+		if string(t) != "*multipart.FileHeader" {
+			continue
+		}
+
+		inputFieldName := typeField.Tag.Get("form")
+		if inputFieldName == "" {
+			inputFieldName = typeField.Name
+		}
+
+		_, fileHeader, err := req.FormFile(inputFieldName)
+		if err != nil {
+			return err
+		}
+
+		structField.Set(reflect.ValueOf(fileHeader))
+
+	}
+	return nil
+}
+
 var errUnknownType = errors.New("Unknown type")
 
 func mapUri(ptr interface{}, m map[string][]string) error {


Test patch:
diff --git a/binding/binding_test.go b/binding/binding_test.go
index b265af3..ee78822 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -8,9 +8,11 @@ import (
 	"bytes"
 	"encoding/json"
 	"errors"
+	"io"
 	"io/ioutil"
 	"mime/multipart"
 	"net/http"
+	"os"
 	"strconv"
 	"strings"
 	"testing"
@@ -31,6 +33,18 @@ type FooBarStruct struct {
 	Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
 }
 
+type FooBarFileStruct struct {
+	FooBarStruct
+	File *multipart.FileHeader `form:"file" binding:"required"`
+}
+
+type FooBarFileFailStruct struct {
+	FooBarStruct
+	File *multipart.FileHeader `invalid_name:"file" binding:"required"`
+	// for unexport test
+	data *multipart.FileHeader `form:"data" binding:"required"`
+}
+
 type FooDefaultBarStruct struct {
 	FooStruct
 	Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"`
@@ -187,8 +201,8 @@ func TestBindingDefault(t *testing.T) {
 	assert.Equal(t, Form, Default("POST", MIMEPOSTForm))
 	assert.Equal(t, Form, Default("PUT", MIMEPOSTForm))
 
-	assert.Equal(t, Form, Default("POST", MIMEMultipartPOSTForm))
-	assert.Equal(t, Form, Default("PUT", MIMEMultipartPOSTForm))
+	assert.Equal(t, FormMultipart, Default("POST", MIMEMultipartPOSTForm))
+	assert.Equal(t, FormMultipart, Default("PUT", MIMEMultipartPOSTForm))
 
 	assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
 	assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
@@ -536,6 +550,54 @@ func createFormPostRequestForMapFail(t *testing.T) *http.Request {
 	return req
 }
 
+func createFormFilesMultipartRequest(t *testing.T) *http.Request {
+	boundary := "--testboundary"
+	body := new(bytes.Buffer)
+	mw := multipart.NewWriter(body)
+	defer mw.Close()
+
+	assert.NoError(t, mw.SetBoundary(boundary))
+	assert.NoError(t, mw.WriteField("foo", "bar"))
+	assert.NoError(t, mw.WriteField("bar", "foo"))
+
+	f, err := os.Open("form.go")
+	assert.NoError(t, err)
+	defer f.Close()
+	fw, err1 := mw.CreateFormFile("file", "form.go")
+	assert.NoError(t, err1)
+	io.Copy(fw, f)
+
+	req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
+	assert.NoError(t, err2)
+	req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
+
+	return req
+}
+
+func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
+	boundary := "--testboundary"
+	body := new(bytes.Buffer)
+	mw := multipart.NewWriter(body)
+	defer mw.Close()
+
+	assert.NoError(t, mw.SetBoundary(boundary))
+	assert.NoError(t, mw.WriteField("foo", "bar"))
+	assert.NoError(t, mw.WriteField("bar", "foo"))
+
+	f, err := os.Open("form.go")
+	assert.NoError(t, err)
+	defer f.Close()
+	fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go")
+	assert.NoError(t, err1)
+	io.Copy(fw, f)
+
+	req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
+	assert.NoError(t, err2)
+	req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
+
+	return req
+}
+
 func createFormMultipartRequest(t *testing.T) *http.Request {
 	boundary := "--testboundary"
 	body := new(bytes.Buffer)
@@ -613,6 +675,34 @@ func TestBindingFormPostForMapFail(t *testing.T) {
 	assert.Error(t, err)
 }
 
+func TestBindingFormFilesMultipart(t *testing.T) {
+	req := createFormFilesMultipartRequest(t)
+	var obj FooBarFileStruct
+	FormMultipart.Bind(req, &obj)
+
+	// file from os
+	f, _ := os.Open("form.go")
+	defer f.Close()
+	fileActual, _ := ioutil.ReadAll(f)
+
+	// file from multipart
+	mf, _ := obj.File.Open()
+	defer mf.Close()
+	fileExpect, _ := ioutil.ReadAll(mf)
+
+	assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
+	assert.Equal(t, obj.Foo, "bar")
+	assert.Equal(t, obj.Bar, "foo")
+	assert.Equal(t, fileExpect, fileActual)
+}
+
+func TestBindingFormFilesMultipartFail(t *testing.T) {
+	req := createFormFilesMultipartRequestFail(t)
+	var obj FooBarFileFailStruct
+	err := FormMultipart.Bind(req, &obj)
+	assert.Error(t, err)
+}
+
 func TestBindingFormMultipart(t *testing.T) {
 	req := createFormMultipartRequest(t)
 	var obj FooBarStruct