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


77

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

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

它工作正常,但是我正在尝试将此请求转换为我的golang程序。

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

我尝试了此链接和许多其他链接,但是,对于我尝试的每个代码,服务器的响应都是“未发送图像”,我也不知道为什么。如果有人知道上面的示例正在发生什么。


1
您能否提供有关服务器的详细信息?
nvcnvn

6
发布不起作用的代码。
2013年

Answers:


145

这是一些示例代码。

简而言之,您将需要使用该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
}

1
好的示例代码。缺少一件事:某些Web服务器(例如Django)检查部件的“ Content-Type”标头。设置标头的方法如下:<pre> partHeader:= textproto.MIMEHeader {} disp:= fmt.Sprintf(form-data; name="data"; filename="%s",fn)partHeader.Add(“ Content-Disposition”,disp)partHeader.Add(“ Content-Type”, “ image / jpeg”)部分,错误:= writer.CreatePart(partHeader)</ pre>
Yu

效果很好,除了file.Name()不适用于我。似乎它返回传递给os.Open()(string) (len=37) "./internal/file_example_JPG_500kB.jpg" // Name returns the name of the file as presented to Open. func (f *File) Name() string { return f.name }的文件路径而不是名称...如果我对其中的文件名进行硬编码,w.CreateFormFile()则可以正常工作。感谢Attila
Lukas Lukac

抱歉,如果我们的值类似于map [string] [] string怎么办?
alikhan'9

4

这是我使用的函数,用于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
}

2

在必须解码此问题的可接受答案以用于单元测试之后,我最终得到了以下重构代码:

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's

0

为了扩展@ attila-o的答案,这是我在Go with中执行POST HTTP req所需的代码:

  • 1档
  • 可配置的文件名(f.Name()无效)
  • 额外的表格栏位。

卷毛表示:

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
}

0

在单元测试中也有同样的问题,如果您只需要发送数据以验证该帖子(以下)对我来说简单一点。希望它可以帮助节省一些时间。

fileReader := strings.NewReader("log file contents go here")
b := bytes.Buffer{} // buffer to write the request payload into
fw := multipart.NewWriter(&b)
fFile, _ := fw.CreateFormFile("file", "./logfile.log")
io.Copy(fFile, fileReader)
fw.Close()

req, _ := http.NewRequest(http.MethodPost, url, &b)
req.Header.Set("Content-Type", fw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

对我来说,fileReader只是一个字符串读取器,因为我要发布一个日志文件。对于图像,您将发送适当的阅读器。


-2

我找到了本教程非常有助于阐明我对Go中文件上传的困惑。

基本上,您是form-data在客户端上使用ajax上传文件,并在服务器上使用以下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.RequestPS这只是将文件存储在同一文件夹中,并且不执行任何安全检查。


8
OP正在询问如何使用Go(HTTP客户端)发布文件,而不是接受和处理从Go(HTTP服务器)中的网页发布的文件。
Mike Atlas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.