-
Star
(234)
You must be signed in to star a gist -
Fork
(55)
You must be signed in to fork a gist
-
-
Save mattetti/5914158 to your computer and use it in GitHub Desktop.
| package main | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "log" | |
| "mime/multipart" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| ) | |
| // Creates a new file upload http request with optional extra params | |
| func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) { | |
| file, err := os.Open(path) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer file.Close() | |
| body := &bytes.Buffer{} | |
| writer := multipart.NewWriter(body) | |
| part, err := writer.CreateFormFile(paramName, filepath.Base(path)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| _, err = io.Copy(part, file) | |
| for key, val := range params { | |
| _ = writer.WriteField(key, val) | |
| } | |
| err = writer.Close() | |
| if err != nil { | |
| return nil, err | |
| } | |
| req, err := http.NewRequest("POST", uri, body) | |
| req.Header.Set("Content-Type", writer.FormDataContentType()) | |
| return req, err | |
| } | |
| func main() { | |
| path, _ := os.Getwd() | |
| path += "/test.pdf" | |
| extraParams := map[string]string{ | |
| "title": "My Document", | |
| "author": "Matt Aimonetti", | |
| "description": "A document with all the Go programming language secrets", | |
| } | |
| request, err := newfileUploadRequest("https://google.com/upload", extraParams, "file", "/tmp/doc.pdf") | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| client := &http.Client{} | |
| resp, err := client.Do(request) | |
| if err != nil { | |
| log.Fatal(err) | |
| } else { | |
| body := &bytes.Buffer{} | |
| _, err := body.ReadFrom(resp.Body) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| resp.Body.Close() | |
| fmt.Println(resp.StatusCode) | |
| fmt.Println(resp.Header) | |
| fmt.Println(body) | |
| } | |
| } |
@themihai I'm wondering it too, do you have solution?
My two cents: using file.Stat is an triggers an unnecessary system call. You could use filepath.Base(path) in order to obtain the filename.
great solution @bacongobbler, big thanks
I'm having similar issue as @vicbaily528 where the content type is an octet stream. Any ideas how to get around this?
var bodyContent []byte
resp.Body.Read(bodyContent)
resp.Body.Close()
fmt.Println(bodyContent)not work,
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(res)
fmt.Println(string(body))work well
Didn't work at first. Just used wireshark and found there is no content-type in the request.
Thanks for this example.
It helped a lot.
However, if I may, I'd like to contribute to this discussion in the hope that it'll be of use to others and save them time.
As was already pointed out by @ghost when it comes to large files, it's inconvenient to use a buffer, as it consumes a lot of resources.
Luckily, there's a solution. After googling for a while I encountered this article where the idea of in-memory piping is leveraged for multipart uploading.
The advantages of this approach are tremendous in my point of view.
Another example can be found here.
I hope it'll be helpful.
Cheers.
Thanks a lot!
I had the same problem as @vicbaily528 and @melissafzhang, after take a look at mime/multipart/writer.go I decide to replace the CreateFormFile function for a custom one:
func MyCreateFormFile(fieldname, filename, contentType string) (io.Writer, error) {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
h.Set("Content-Type", escapeQuotes(contentType))
return w.CreatePart(h)
}
this worked for me, I hope it can help you or others who have the same problem.
Yet another example:
func uploadFileMultipart(url string, path string) (*http.Response, error) {
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
// Reduce number of syscalls when reading from disk.
bufferedFileReader := bufio.NewReader(f)
defer f.Close()
// Create a pipe for writing from the file and reading to
// the request concurrently.
bodyReader, bodyWriter := io.Pipe()
formWriter := multipart.NewWriter(bodyWriter)
// Store the first write error in writeErr.
var (
writeErr error
errOnce sync.Once
)
setErr := func(err error) {
if err != nil {
errOnce.Do(func() { writeErr = err })
}
}
go func() {
partWriter, err := formWriter.CreateFormFile("file", path)
setErr(err)
_, err = io.Copy(partWriter, bufferedFileReader)
setErr(err)
setErr(formWriter.Close())
setErr(bodyWriter.Close())
}()
req, err := http.NewRequest(http.MethodPut, url, bodyReader)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", formWriter.FormDataContentType())
// This operation will block until both the formWriter
// and bodyWriter have been closed by the goroutine,
// or in the event of a HTTP error.
resp, err := http.DefaultClient.Do(req)
if writeErr != nil {
return nil, writeErr
}
return resp, err
}thank you.
Can I upload two or more files one times?
@donnol - jep, just call the "createFormFile" function multiple times
"https://google.com/upload" not found
Thanks a lot.. This is really useful.
Really useful, it works
@sebnyberg Hello, just wanted to understand about the comment "This operation will block until both the formWriter...". Could you please explain, thank you!
@sebnyberg Hello, just wanted to understand about the comment "This operation will block until both the formWriter...". Could you please explain, thank you!
Sure. The request reads from the provided reader until it returns io.EOF. For an io.Pipe, the reader-end will return io.EOF after the write-end is closed. That's what is meant by "blocking". If the bodyWriter is not closed, the the request will last forever (until conn timeout).
https://pkg.go.dev/io#PipeWriter.Close
Closing the formWriter isn't strictly necessary to send the request. However, the formWriters Close() writes a trailer to the multipart message that is required for the request to be valid.
@sebnyberg Thank you very much! :)
If uploading specific format like gzip or server expect content other than octet-stream, better to use writer.CreatePart instead of formWriter.CreateFormFile
part, err := writer.CreatePart(textproto.MIMEHeader{
"Content-Type": []string{"application/x-gzip"},
"Content-Disposition": []string{fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
"file", fi.Name())},
})
One of the core problems with the original example is that it requires buffering all data into into memory, which doesn't work very well if you're dealing with files.
However, it's non-obvious how to use the multi-part form in a streaming fashion - in part because the design of it is to enclose the various parts with a header and footer, which doesn't lend itself well to a typical streaming interface.
Instead, you need to create a pipe connected to the form and then and then io.Copy each file into that pipe after setting the headers, and then close it all out at the end.
Key notes:
- need to create a Pipe with which to construct the http request
- create a form using the pipe
- set boundary header
- start a goroutine that begins feeding the pipe (i.e. with a file) before
Do()ing the request - must call form.Close() to add the final boundary and complete the body
// Written in 2026 by AJ ONeal <aj@therootcompany.com> with assistance by Grok Ai.
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along with
// this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
func (c *Client) UploadFile(folderID string, r io.ReadCloser, filename string) (*FileInfo, error) {
if filename == "" {
_ = r.Close()
return nil, errors.New("filename is required")
}
defer func() { _ = r.Close() }()
token, err := c.getToken()
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
form := multipart.NewWriter(pw)
defer func() {
// safe to do here because the goroutine must end before the request
_ = pr.Close()
_ = pw.Close()
_ = form.Close()
}()
// fmt.Println("DEBUG create request", uploadURL)
uploadURL := DefaultUploadURL
req, err := http.NewRequest("POST", uploadURL, pr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "multipart/form-data; boundary="+form.Boundary())
// Since the file can only be copied once it's already being read,
// we need this goroutine to allow us to submit the request.
// This wouldn't be necessary if we were just uploading a file that
// could be read from directly, or if we didn't have the final boundary
// and could create use a concatenating reader
go func() {
// meta data
part, _ := form.CreateFormField("attributes")
attributes := map[string]any{
"name": filename,
"parent": map[string]string{"id": folderID},
}
attrsJSON, _ := json.Marshal(attributes)
if _, err = part.Write(attrsJSON); err != nil {
pw.CloseWithError(err)
return
}
// fmt.Println("DEBUG wrote attrs")
// file data
filePart, _ := form.CreateFormFile("file", filename)
if _, err = io.Copy(filePart, r); err != nil {
pw.CloseWithError(err)
return
}
// fmt.Println("DEBUG wrote file")
// form data
if err := form.Close(); err != nil {
pw.CloseWithError(err)
return
}
// fmt.Println("DEBUG wrote form")
}()
// fmt.Println("DEBUG do request")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("upload request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
// fmt.Println("DEBUG got response")
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("upload failed (%s): %s", resp.Status, string(bodyBytes))
}
var uploadResp struct {
Entries []FileInfo `json:"entries"`
}
if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
return nil, fmt.Errorf("decode upload response: %w", err)
}
if len(uploadResp.Entries) == 0 {
return nil, errors.New("no file entry in upload response")
}
return &uploadResp.Entries[0], nil
}
server use multipart to response to client
https://gist.github.com/shuaihanhungry/9c65d3d1e866faf6bef08152b078c519