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

Epi*_*lle 62 curl file-upload multipart go

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

curl -F "image"=@"IMAGEFILE" -F "key"="KEY" URL
Run Code Online (Sandbox Code Playgroud)

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

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

我尝试了这个链接和许多其他链接,但是,对于我尝试的每个代码,来自服务器的响应是"没有图像发送",我不知道为什么.如果有人知道上面的例子发生了什么.

谢谢

Att*_* O. 119

这是一些示例代码.

简而言之,您需要使用该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
}
Run Code Online (Sandbox Code Playgroud)

  • 很好的示例代码。缺少一件事:某些 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") part, err := writer.CreatePart(partHeader) </pre> (2认同)
  • 您在“mustOpen()”中打开文件,但不要关闭它。使用完文件后是否应该使用“defer file.Close()”关闭文件? (2认同)

Luk*_*ton 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
}

Run Code Online (Sandbox Code Playgroud)

现在它应该很容易使用:

    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())
Run Code Online (Sandbox Code Playgroud)


小智 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
}
Run Code Online (Sandbox Code Playgroud)

例子:

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)
}
Run Code Online (Sandbox Code Playgroud)

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


Pat*_*ick 5

这是我使用的一个函数,用于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
}
Run Code Online (Sandbox Code Playgroud)