gofile-uploadmultipartform-datago-http

Golang Uploading Big file to external API with multipart. How to avoid `io.Copy(io.Writer, io.Reader)` problem


My objective is to upload a big file to POST https://somehost/media using golang's builtin net/http package.

HTTP format of the Api call

POST /media HTTP/1.1
Host: somehost
Content-Length: 434
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="detail"

More and more detail
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="some_big_video.mp4"
Content-Type: <Content-Type header here>

(data)
------WebKitFormBoundary7MA4YWxkTrZu0gW--

in golang here is the code.


package main

import (
  "fmt"
  "bytes"
  "mime/multipart"
  "os"
  "path/filepath"
  "io"
  "net/http"
  "io/ioutil"
)

func main() {

  url := "https://somehost/media"
  method := "POST"

  payload := &bytes.Buffer{}
  writer := multipart.NewWriter(payload)
  _ = writer.WriteField("details", "more and more details")
  file, errFile3 := os.Open("/Users/vajahat/Downloads/some_big_video.mp4")
  defer file.Close()
  part3,errFile3 := writer.CreateFormFile("file","some_big_video.mp4") 
  _, errFile3 = io.Copy(part3, file)
  if errFile3 != nil {
    fmt.Println(errFile3)
    return
  }
  err := writer.Close()
  if err != nil {
    fmt.Println(err)
    return
  }
  client := &http.Client {}
  req, err := http.NewRequest(method, url, payload)

  if err != nil {
    fmt.Println(err)
    return
  }

  req.Header.Set("Content-Type", writer.FormDataContentType())
  res, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, err := ioutil.ReadAll(res.Body)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(string(body))
}

How to avoid io.Copy(io.Writer, io.Reader) problem

The above code is working fine however on the line _, errFile3 = io.Copy(part3, file). This literally copies everything from file in to main memory.

How can this be avoided?

Is there any way, i can stream the big file via multipart-formdata to an api?

This program will be runing on remote server. And that can crash if a very big file is opened.


Solution

  • Use an io.Pipe and a goroutine to copy the file to the request without loading the entire file in memory.

    pr, pw := io.Pipe()
    writer := multipart.NewWriter(pw)
    ct := writer.FormDataContentType()
    go func() {
        _ = writer.WriteField("details", "more and more details")
        file, err := os.Open("/Users/vajahat/Downloads/some_big_video.mp4")
        if err != nil {
            pw.CloseWithError(err)
            return
        }
        defer file.Close()
        part3, err := writer.CreateFormFile("file", "some_big_video.mp4")
        if err != nil {
            pw.CloseWithError(err)
            return
        }
        _, err = io.Copy(part3, file)
        if err != nil {
            pw.CloseWithError(err)
            return
        }
        pw.CloseWithError(writer.Close())
    }()
    
    client := &http.Client{}
    req, err := http.NewRequest(method, url, pr)
    
    if err != nil {
        fmt.Println(err)
        return
    }
    
    req.Header.Set("Content-Type", ct)
    // remaining code as before