From 737cf3d95712bfa4a4b6fe43be5affd83e1be4a1 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 21 Sep 2017 10:18:34 +0100 Subject: [PATCH] rest: factor multipart upload out into function and generalise --- rest/rest.go | 124 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 44 deletions(-) diff --git a/rest/rest.go b/rest/rest.go index c443d2269..f754f3ddb 100644 --- a/rest/rest.go +++ b/rest/rest.go @@ -93,9 +93,10 @@ type Opts struct { Password string // password for Basic Auth Options []fs.OpenOption IgnoreStatus bool // if set then we don't check error status or parse error body - MultipartMetadataName string // set the following 3 vars - MultipartContentName string // and Body and pass in request - MultipartFileName string // for multipart upload + MultipartParams url.Values // if set do multipart form upload with attached file + MultipartMetadataName string // ..this is used for the name of the metadata form part if set + MultipartContentName string // ..name of the parameter which is the attached file + MultipartFileName string // ..name of the file for the attached file Parameters url.Values // any parameters for the final URL TransferEncoding []string // transfer encoding, set to "identity" to disable chunked encoding Close bool // set to close the connection after this transaction @@ -219,59 +220,100 @@ func (api *Client) Call(opts *Opts) (resp *http.Response, err error) { return resp, nil } +// MultipartUpload creates an io.Reader which produces an encoded a +// multipart form upload from the params passed in and the passed in +// +// in - the body of the file +// params - the form parameters +// fileName - is the name of the attached file +// contentName - the name of the parameter for the file +// +// NB This doesn't allow setting the content type of the attachment +func MultipartUpload(in io.Reader, params url.Values, contentName, fileName string) (io.ReadCloser, string, error) { + bodyReader, bodyWriter := io.Pipe() + writer := multipart.NewWriter(bodyWriter) + contentType := writer.FormDataContentType() + + // Pump the data in the background + go func() { + var err error + + for key, vals := range params { + for _, val := range vals { + err = writer.WriteField(key, val) + if err != nil { + _ = bodyWriter.CloseWithError(errors.Wrap(err, "create metadata part")) + return + } + } + } + + part, err := writer.CreateFormFile(contentName, fileName) + if err != nil { + _ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to create form file")) + return + } + + _, err = io.Copy(part, in) + if err != nil { + _ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to copy data")) + return + } + + err = writer.Close() + if err != nil { + _ = bodyWriter.CloseWithError(errors.Wrap(err, "failed to close form")) + return + } + + _ = bodyWriter.Close() + }() + + return bodyReader, contentType, nil +} + // CallJSON runs Call and decodes the body as a JSON object into response (if not nil) // // If request is not nil then it will be JSON encoded as the body of the request // +// If (opts.MultipartParams or opts.MultipartContentName) and +// opts.Body are set then CallJSON will do a multipart upload with a +// file attached. opts.MultipartContentName is the name of the +// parameter and opts.MultipartFileName is the name of the file. If +// MultpartContentName is set, and request != nil is supplied, then +// the request will be marshalled into JSON and added to the form with +// parameter name MultipartMetadataName. +// // It will return resp if at all possible, even if err is set func (api *Client) CallJSON(opts *Opts, request interface{}, response interface{}) (resp *http.Response, err error) { var requestBody []byte // Marshal the request if given if request != nil { - opts = opts.Copy() requestBody, err = json.Marshal(request) - opts.ContentType = "application/json" if err != nil { return nil, err } - // Set the body up as a JSON object if required + // Set the body up as a JSON object if no body passed in if opts.Body == nil { + opts = opts.Copy() + opts.ContentType = "application/json" opts.Body = bytes.NewBuffer(requestBody) } } - errChan := make(chan error, 1) - isMultipart := opts.MultipartMetadataName != "" && opts.Body != nil && request != nil + isMultipart := (opts.MultipartParams != nil || opts.MultipartMetadataName != "") && opts.Body != nil if isMultipart { - bodyReader, bodyWriter := io.Pipe() - writer := multipart.NewWriter(bodyWriter) - opts.ContentType = writer.FormDataContentType() - in := opts.Body - opts.Body = bodyReader - go func() { - defer func() { _ = bodyWriter.Close() }() - var err error - - // Create the first part - err = writer.WriteField(opts.MultipartMetadataName, string(requestBody)) - if err != nil { - errChan <- err - return - } - - // Add the file part - part, err := writer.CreateFormFile(opts.MultipartContentName, opts.MultipartFileName) - if err != nil { - errChan <- err - return - } - - // Copy it in - if _, err := io.Copy(part, in); err != nil { - errChan <- err - return - } - errChan <- writer.Close() - }() + params := opts.MultipartParams + if params == nil { + params = url.Values{} + } + if opts.MultipartMetadataName != "" { + params.Add(opts.MultipartMetadataName, string(requestBody)) + } + opts = opts.Copy() + opts.Body, opts.ContentType, err = MultipartUpload(opts.Body, params, opts.MultipartContentName, opts.MultipartFileName) + if err != nil { + return nil, err + } } resp, err = api.Call(opts) if err != nil { @@ -280,12 +322,6 @@ func (api *Client) CallJSON(opts *Opts, request interface{}, response interface{ if response == nil || opts.NoResponse { return resp, nil } - if isMultipart { - err = <-errChan - if err != nil { - return resp, err - } - } err = DecodeJSON(resp, response) return resp, err }