diff --git a/client.go b/client.go index be9f4377..e7bb0978 100644 --- a/client.go +++ b/client.go @@ -300,19 +300,20 @@ func (c *Client) SetAuthToken(token string) *Client { // R method creates a request instance, its used for Get, Post, Put, Delete, Patch, Head and Options. func (c *Client) R() *Request { r := &Request{ - URL: "", - Method: "", - QueryParam: url.Values{}, - FormData: url.Values{}, - Header: http.Header{}, - Body: nil, - Result: nil, - Error: nil, - RawRequest: nil, - client: c, - bodyBuf: nil, - multipartFiles: []*File{}, - pathParams: make(map[string]string), + URL: "", + Method: "", + QueryParam: url.Values{}, + FormData: url.Values{}, + Header: http.Header{}, + Body: nil, + Result: nil, + Error: nil, + RawRequest: nil, + client: c, + bodyBuf: nil, + multipartFiles: []*File{}, + multipartFields: []*multipartField{}, + pathParams: make(map[string]string), } return r @@ -865,3 +866,11 @@ type File struct { func (f *File) String() string { return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name) } + +// multipartField represent custom data part for multipart request +type multipartField struct { + Param string + FileName string + ContentType string + io.Reader +} \ No newline at end of file diff --git a/middleware.go b/middleware.go index 4f556596..a9cdb551 100644 --- a/middleware.go +++ b/middleware.go @@ -311,6 +311,15 @@ func handleMultipart(c *Client, r *Request) (err error) { } } + // GitHub #130 adding multipart field support with content type + if len(r.multipartFields) > 0 { + for _, mf := range r.multipartFields { + if err = addMultipartFormField(w, mf); err != nil { + return + } + } + } + r.Header.Set(hdrContentTypeKey, w.FormDataContentType()) err = w.Close() diff --git a/request.go b/request.go index bad822d1..97f07e7d 100644 --- a/request.go +++ b/request.go @@ -281,6 +281,20 @@ func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Reque return r } +// SetMultipartField method is to set custom data using io.Reader for multipart upload. +func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request { + r.isMultiPart = true + + r.multipartFields = append(r.multipartFields, &multipartField{ + Param: param, + FileName: fileName, + ContentType: contentType, + Reader: reader, + }) + + return r +} + // SetContentLength method sets the HTTP header `Content-Length` value for current request. // By default go-resty won't set `Content-Length`. Also you have an option to enable for every // request. See `resty.SetContentLength` diff --git a/request16.go b/request16.go index 2b06766c..1e229b7f 100644 --- a/request16.go +++ b/request16.go @@ -43,6 +43,7 @@ type Request struct { isSaveResponse bool outputFile string multipartFiles []*File + multipartFields []*multipartField notParseResponse bool fallbackContentType string pathParams map[string]string diff --git a/request17.go b/request17.go index 387957e3..30ddc448 100644 --- a/request17.go +++ b/request17.go @@ -44,6 +44,7 @@ type Request struct { isSaveResponse bool outputFile string multipartFiles []*File + multipartFields []*multipartField notParseResponse bool ctx context.Context fallbackContentType string diff --git a/request_test.go b/request_test.go index a8853823..9644ca3b 100644 --- a/request_test.go +++ b/request_test.go @@ -613,6 +613,25 @@ func TestMultiPartUploadFileNotOnGetOrDelete(t *testing.T) { assertEqual(t, "multipart content is not allowed in HTTP verb [DELETE]", err.Error()) } +func TestMultiPartMultipartField(t *testing.T) { + ts := createFormPostServer(t) + defer ts.Close() + defer cleanupFiles("test-data/upload") + + jsonBytes := []byte(`{"input": {"name": "Uploaded document", "_filename" : ["file.txt"]}}`) + + resp, err := dclr(). + SetFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M"}). + SetMultipartField("uploadManifest", "upload-file.json", "application/json", bytes.NewReader(jsonBytes)). + Post(ts.URL + "/upload") + + responseStr := resp.String() + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + assertEqual(t, true, strings.Contains(responseStr, "upload-file.json")) +} + func TestGetWithCookie(t *testing.T) { ts := createGetServer(t) defer ts.Close() diff --git a/util.go b/util.go index 5b7f6096..cd6a0dc0 100644 --- a/util.go +++ b/util.go @@ -107,6 +107,24 @@ func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } +func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader { + hdr := make(textproto.MIMEHeader) + hdr.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + escapeQuotes(param), escapeQuotes(fileName))) + hdr.Set("Content-Type", contentType) + return hdr +} + +func addMultipartFormField(w *multipart.Writer, mf *multipartField) error { + partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType)) + if err != nil { + return err + } + + _, err = io.Copy(partWriter, mf.Reader) + return err +} + func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error { // Auto detect actual multipart content type cbuf := make([]byte, 512) @@ -115,11 +133,7 @@ func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r i return err } - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, - escapeQuotes(fieldName), escapeQuotes(fileName))) - h.Set("Content-Type", http.DetectContentType(cbuf)) - partWriter, err := w.CreatePart(h) + partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf))) if err != nil { return err }