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

时间:2013-11-25 23:42:37

标签: file-upload curl go multipart

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

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

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

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

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

10 个答案:

答案 0 :(得分:99)

以下是一些示例代码。

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

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 :(得分:1)

这是我使用过的函数,它使用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 :(得分: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
}

答案 3 :(得分:0)

关于attila -o的帖子,请求标题没有边界,因为Writer已经关闭。

// after the close, the bounday will be nil.
w.Close()
...
req.Header.Set("Content-Type", w.FormDataContentType())

所以,我认为它应该在集合之后关闭。

req.Header.Set("Content-Type", w.FormDataContentType())
w.Close()

答案 4 :(得分:0)

我发现this tutorial非常有助于澄清我对Go中文件上传的疑惑。

基本上,您在客户端使用myArray通过ajax上传文件,并在服务器上使用以下一小段Go代码:

form-data

此处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 P.S。这只会将文件存储在同一文件夹中,并且不会执行任何安全检查。

答案 5 :(得分:0)

也许这可能很有用https://github.com/030/go-curl

./go-curl -url \
    http://localhost:9999/service/rest/v1/components?repository=maven-releases \
    -user admin -pass admin123 -F \
    "maven2.asset1=@utils/test-files-multipart/file1.pom,\
    maven2.asset1.extension=pom,\
    maven2.asset2=@utils/test-files-multipart/file1.jar,\
    maven2.asset2.extension=jar,\
    maven2.asset3=@utils/test-files-multipart/file1-sources.jar,\
    maven2.asset3.extension=sources.jar"

答案 6 :(得分:0)

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

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

答案 7 :(得分:0)

要扩展@ attila-o答案,这是我在Go with中执行POST HTTP请求所用的代码:

  • 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
}

答案 8 :(得分: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只是一个字符串读取器,因为我正在发布一个日志文件。如果是图片,则应发送适当的阅读器。

答案 9 :(得分:0)

这是一个适用于文件或字符串的选项:

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