使用Content-Type为multipart/form-data的POST数据

108

我正在尝试使用go从我的计算机上传图片到一个网站。通常,我使用一个bash脚本将文件和密钥发送到服务器:


curl -F "image"=@"IMAGEFILE" -F "key"="KEY" URL

功能正常,但我正在尝试将此请求转换为我的golang程序。

http://matt.aimonetti.net/posts/2013/07/01/golang-multipart-file-upload-example/

我尝试过这个链接和其他许多链接,但是每次尝试的代码,服务器的响应都是“未发送图像”,而我不知道为什么。如果有人知道上面例子里发生了什么,请告诉我。


3
好的,下面是内容的翻译:请问如何在Go语言中进行HTTP POST文件上传(multipart编程)?这个问题已经在stackoverflow上有了一个类似的问题:https://dev59.com/mlrUa4cB1Zd3GeqPmbjp - rofrol
你可以使用 https://curlconverter.com/go/ 将那个 curl 命令转换为 Go。 - user3064538
7个回答

200

这里是一些示例代码。

简而言之,您需要使用mime/multipart来构建表单。

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "net/http/httputil"
    "os"
    "strings"
)

func main() {

    var client *http.Client
    var remoteURL string
    {
        //setup a mocked http client.
        ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            b, err := httputil.DumpRequest(r, true)
            if err != nil {
                panic(err)
            }
            fmt.Printf("%s", b)
        }))
        defer ts.Close()
        client = ts.Client()
        remoteURL = ts.URL
    }

    //prepare the reader instances to encode
    values := map[string]io.Reader{
        "file":  mustOpen("main.go"), // lets assume its this file
        "other": strings.NewReader("hello world!"),
    }
    err := Upload(client, remoteURL, values)
    if err != nil {
        panic(err)
    }
}

func Upload(client *http.Client, url string, values map[string]io.Reader) (err error) {
    // Prepare a form that you will submit to that URL.
    var b bytes.Buffer
    w := multipart.NewWriter(&b)
    for key, r := range values {
        var fw io.Writer
        if x, ok := r.(io.Closer); ok {
            defer x.Close()
        }
        // Add an image file
        if x, ok := r.(*os.File); ok {
            if fw, err = w.CreateFormFile(key, x.Name()); err != nil {
                return
            }
        } else {
            // Add other fields
            if fw, err = w.CreateFormField(key); err != nil {
                return
            }
        }
        if _, err = io.Copy(fw, r); err != nil {
            return err
        }

    }
    // Don't forget to close the multipart writer.
    // If you don't close it, your request will be missing the terminating boundary.
    w.Close()

    // Now that you have a form, you can submit it to your handler.
    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }
    // Don't forget to set the content type, this will contain the boundary.
    req.Header.Set("Content-Type", w.FormDataContentType())

    // Submit the request
    res, err := client.Do(req)
    if err != nil {
        return
    }

    // Check the response
    if res.StatusCode != http.StatusOK {
        err = fmt.Errorf("bad status: %s", res.Status)
    }
    return
}

func mustOpen(f string) *os.File {
    r, err := os.Open(f)
    if err != nil {
        panic(err)
    }
    return r
}

3
好的示例代码。有一件事情缺失了:某些 Web 服务器(例如 Django)会检查部分的 "Content-Type" 头信息。以下是如何设置该头信息的方法:<pre> partHeader := textproto.MIMEHeader{} disp := fmt.Sprintf(form-data; name=&quot;data&quot;; filename=&quot;%s&quot;, fn) partHeader.Add("Content-Disposition", disp) partHeader.Add("Content-Type", "image/jpeg") part, err := writer.CreatePart(partHeader) </pre> - Zhichang Yu
除了 file.Name() 不起作用之外,其他都很好。似乎它返回的是传递给 os.Open() 的文件路径而不是名称... (string) (len=37) "./internal/file_example_JPG_500kB.jpg" // Name 返回打开时文件的名称。 func (f *File) Name() string { return f.name } 如果我在 w.CreateFormFile() 中硬编码文件名,它就可以正常工作。谢谢 Attila - Lukas Lukac
抱歉,如果我们的值是map[string][]string类型怎么办? - alikhan
2
你在 mustOpen() 中打开了文件,但没有关闭它。你应该在完成后使用 defer file.Close() 关闭文件吗? - lion_bash
1
@lion_bash 这是真的;回想起来,我写的这段代码几乎是十年前的了,非常糟糕。不过你可以提出修改建议,因为你似乎有这样的声誉 :) - Attila O.
1
这也是如何“不”编写干净代码的示例... - Lucas

14

这是我使用的一个函数,它使用 io.Pipe() 来避免将整个文件读入内存或需要管理任何缓冲区。它仅处理单个文件,但可以通过在 goroutine 中添加更多部分轻松扩展以处理更多文件。正常情况下效果很好。错误路径还没有进行充分测试。

import (
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func UploadMultipartFile(client *http.Client, uri, key, path string) (*http.Response, error) {
    body, writer := io.Pipe()

    req, err := http.NewRequest(http.MethodPost, uri, body)
    if err != nil {
        return nil, err
    }

    mwriter := multipart.NewWriter(writer)
    req.Header.Add("Content-Type", mwriter.FormDataContentType())

    errchan := make(chan error)

    go func() {
        defer close(errchan)
        defer writer.Close()
        defer mwriter.Close()

        w, err := mwriter.CreateFormFile(key, path)
        if err != nil {
            errchan <- err
            return
        }

        in, err := os.Open(path)
        if err != nil {
            errchan <- err
            return
        }
        defer in.Close()

        if written, err := io.Copy(w, in); err != nil {
            errchan <- fmt.Errorf("error copying %s (%d bytes written): %v", path, written, err)
            return
        }

        if err := mwriter.Close(); err != nil {
            errchan <- err
            return
        }
    }()

    resp, err := client.Do(req)
    merr := <-errchan

    if err != nil || merr != nil {
        return resp, fmt.Errorf("http error: %v, multipart error: %v", err, merr)
    }

    return resp, nil
}

这对我不起作用。这是我的代码,也许我做错了什么。使用bytes.Buffer并在发出请求之前创建表单,就像接受的答案中所示,可以解决问题。 - Teodor Maxim

6

以下是适用于文件或字符串的选项:

package main

import (
   "bytes"
   "io"
   "mime/multipart"
   "os"
   "strings"
)

func createForm(form map[string]string) (string, io.Reader, error) {
   body := new(bytes.Buffer)
   mp := multipart.NewWriter(body)
   defer mp.Close()
   for key, val := range form {
      if strings.HasPrefix(val, "@") {
         val = val[1:]
         file, err := os.Open(val)
         if err != nil { return "", nil, err }
         defer file.Close()
         part, err := mp.CreateFormFile(key, val)
         if err != nil { return "", nil, err }
         io.Copy(part, file)
      } else {
         mp.WriteField(key, val)
      }
   }
   return mp.FormDataContentType(), body, nil
}

示例:

package main
import "net/http"

func main() {
   form := map[string]string{"image": "@IMAGEFILE", "key": "KEY"}
   ct, body, err := createForm(form)
   if err != nil {
      panic(err)
   }
   http.Post("https://stackoverflow.com", ct, body)
}

https://golang.org/pkg/mime/multipart#Writer.WriteField


你从磁盘中读取文件吗?如果我有一个图像字节并想上传它,该怎么办? - Arjen

6

在我的单元测试中,为了使用这个问题的接受答案,我最终得到了以下重构代码:

func createMultipartFormData(t *testing.T, fieldName, fileName string) (bytes.Buffer, *multipart.Writer) {
    var b bytes.Buffer
    var err error
    w := multipart.NewWriter(&b)
    var fw io.Writer
    file := mustOpen(fileName)
    if fw, err = w.CreateFormFile(fieldName, file.Name()); err != nil {
        t.Errorf("Error creating writer: %v", err)
    }
    if _, err = io.Copy(fw, file); err != nil {
        t.Errorf("Error with io.Copy: %v", err)
    }
    w.Close()
    return b, w
}

func mustOpen(f string) *os.File {
    r, err := os.Open(f)
    if err != nil {
        pwd, _ := os.Getwd()
        fmt.Println("PWD: ", pwd)
        panic(err)
    }
    return r
}


现在使用起来应该非常容易:
    b, w := createMultipartFormData(t, "image","../luke.png")

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }
    // Don't forget to set the content type, this will contain the boundary.
    req.Header.Set("Content-Type", w.FormDataContentType())

1
将文件从一个服务发送到另一个服务:
func UploadFile(network, uri string, f multipart.File, h *multipart.FileHeader) error {

    buf := new(bytes.Buffer)
    writer := multipart.NewWriter(buf)

    part, err := writer.CreateFormFile("file", h.Filename)

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

    b, err := ioutil.ReadAll(f)

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

    part.Write(b)
    writer.Close()

    req, _ := http.NewRequest("POST", uri, buf)

    req.Header.Add("Content-Type", writer.FormDataContentType())
    client := &http.Client{}
    resp, err := client.Do(req)

    if err != nil {
        return err
    }
    defer resp.Body.Close()

    b, _ = ioutil.ReadAll(resp.Body)
    if resp.StatusCode >= 400 {
        return errors.New(string(b))
    }
    return nil
}

也许您可以详细说明一下您的代码是做什么的(包括注释),这将使其成为一个有用的资源,而不仅仅是针对特定问题量身定制的答案。 - chabad360

-1

为了扩展@attila-o的答案,这是我使用的Go代码来执行POST HTTP请求:

  • 1个文件
  • 可配置的文件名(f.Name()无法工作)
  • 额外的表单字段。

Curl表示:

curl -X POST \
  http://localhost:9091/storage/add \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
  -F owner=0xc916Cfe5c83dD4FC3c3B0Bf2ec2d4e401782875e \
  -F password=$PWD \
  -F file=@./internal/file_example_JPG_500kB.jpg

前进方式:

client := &http.Client{
        Timeout: time.Second * 10,
    }
req, err := createStoragePostReq(cfg)
res, err := executeStoragePostReq(client, req)


func createStoragePostReq(cfg Config) (*http.Request, error) {
    extraFields := map[string]string{
        "owner": "0xc916cfe5c83dd4fc3c3b0bf2ec2d4e401782875e",
        "password": "pwd",
    }

    url := fmt.Sprintf("http://localhost:%d%s", cfg.HttpServerConfig().Port(), lethstorage.AddRoute)
    b, w, err := createMultipartFormData("file","./internal/file_example_JPG_500kB.jpg", "file_example_JPG_500kB.jpg", extraFields)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", w.FormDataContentType())

    return req, nil
}

func executeStoragePostReq(client *http.Client, req *http.Request) (lethstorage.AddRes, error) {
    var addRes lethstorage.AddRes

    res, err := client.Do(req)
    if err != nil {
        return addRes, err
    }
    defer res.Body.Close()

    data, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return addRes, err
    }

    err = json.Unmarshal(data, &addRes)
    if err != nil {
        return addRes, err
    }

    return addRes, nil
}

func createMultipartFormData(fileFieldName, filePath string, fileName string, extraFormFields map[string]string) (b bytes.Buffer, w *multipart.Writer, err error) {
    w = multipart.NewWriter(&b)
    var fw io.Writer
    file, err := os.Open(filePath)

    if fw, err = w.CreateFormFile(fileFieldName, fileName); err != nil {
        return
    }
    if _, err = io.Copy(fw, file); err != nil {
        return
    }

    for k, v := range extraFormFields {
        w.WriteField(k, v)
    }

    w.Close()

    return
}

-4

我发现这个教程对于澄清Go中文件上传的困惑非常有帮助。

基本上,你可以通过客户端使用form-data来上传文件,然后在服务器上使用以下简短的Go代码:

file, handler, err := r.FormFile("img") // img is the key of the form-data
if err != nil {
    fmt.Println(err)
    return
}
defer file.Close()

fmt.Println("File is good")
fmt.Println(handler.Filename)
fmt.Println()
fmt.Println(handler.Header)


f, err := os.OpenFile(handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
    fmt.Println(err)
    return
}
defer f.Close()
io.Copy(f, file)

这里的r*http.RequestP.S. 这只是将文件存储在同一文件夹中,不执行任何安全检查。


9
OP 询问如何使用 Go(HTTP 客户端)发布文件,而不是在 Go(HTTP 服务器)中接受和处理从网页 POST 的文件。 - Mike Atlas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接