Skip to content

Instantly share code, notes, and snippets.

@mattetti
Last active August 21, 2024 04:52
Show Gist options
  • Save mattetti/5914158 to your computer and use it in GitHub Desktop.
Save mattetti/5914158 to your computer and use it in GitHub Desktop.
Example of doing a multipart upload in Go (golang)
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)
}
}
@prayuditb
Copy link

@mattetti, thanks for helpful gist and as @mkaz mentioned, adding Content-Type header works for me

@sebnyberg
Copy link

sebnyberg commented Aug 18, 2020

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
}

@faddat
Copy link

faddat commented Sep 27, 2020

thank you.

@donnol
Copy link

donnol commented Oct 15, 2020

Can I upload two or more files one times?

@mirisbowring
Copy link

mirisbowring commented Nov 1, 2020

@donnol - jep, just call the "createFormFile" function multiple times

@toannd96
Copy link

toannd96 commented Mar 5, 2021

@vikfrank
Copy link

Thanks a lot.. This is really useful.

@pablodz
Copy link

pablodz commented Aug 12, 2022

Really useful, it works

@pforpramit
Copy link

@sebnyberg Hello, just wanted to understand about the comment "This operation will block until both the formWriter...". Could you please explain, thank you!

@sebnyberg
Copy link

sebnyberg commented Dec 22, 2022

@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.

https://pkg.go.dev/mime/multipart#Writer.Close

@pforpramit
Copy link

@sebnyberg Thank you very much! :)

@nirajchandak
Copy link

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())},
	})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment