使用 Content-Type 为 multipart/form-data 的方式提交 POST 数据。

huangapple go评论159阅读模式
英文:

POST data using the Content-Type multipart/form-data

问题

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

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

这个方法很有效,但我想将这个请求转换成我的Go语言程序。

我尝试了这个链接和其他很多链接,但是每次我尝试的代码都会得到服务器的响应"no image sent",我不知道为什么会这样。如果有人知道上面的示例出了什么问题,请告诉我。

英文:

I'm trying to upload images from my computer to a website using go. Usually, I use a bash script that sends a file and a key to the server:

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

it works fine, but I'm trying to convert this request into my golang program.

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

I tried this link and many others, but, for each code that I try, the response from the server is "no image sent", and I've no idea why. If someone knows what's happening with the example above.

答案1

得分: 194

以下是示例代码的中文翻译:

这是一些示例代码

简而言之您需要使用 [`mime/multipart` ][1] 来构建表单

[1]: http://golang.org/pkg/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
    	{
    		// 设置一个模拟的 HTTP 客户端。
    		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
    	}
    
    	// 准备读取器实例以进行编码
    	values := map[string]io.Reader{
    		"file":  mustOpen("main.go"), // 假设是这个文件
    		"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) {
    	// 准备一个要提交到该 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()
    		}
    		// 添加一个图像文件
    		if x, ok := r.(*os.File); ok {
    			if fw, err = w.CreateFormFile(key, x.Name()); err != nil {
    				return
    			}
    		} else {
    			// 添加其他字段
    			if fw, err = w.CreateFormField(key); err != nil {
    				return
    			}
    		}
    		if _, err = io.Copy(fw, r); err != nil {
    			return err
    		}
    
    	}
    	// 不要忘记关闭多部分写入器。
    	// 如果不关闭它,您的请求将缺少终止边界。
    	w.Close()
    
    	// 现在您有了一个表单,可以将其提交给处理程序。
    	req, err := http.NewRequest("POST", url, &b)
    	if err != nil {
    		return
    	}
    	// 不要忘记设置内容类型,其中包含边界。
    	req.Header.Set("Content-Type", w.FormDataContentType())
    
    	// 提交请求
    	res, err := client.Do(req)
    	if err != nil {
    		return
    	}
    
    	// 检查响应
    	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
    }

希望对您有所帮助!

英文:

Here's some sample code.

In short, you'll need to use the mime/multipart package to build the form.

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
}

答案2

得分: 13

这是一个我使用过的函数,它使用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
}
英文:

Here's a function I've used that uses io.Pipe() to avoid reading in the entire file to memory or needing to manage any buffers. It handles only a single file, but could easily be extended to handle more by adding more parts within the goroutine. The happy path works well. The error paths have not hand much testing.

import (
&quot;fmt&quot;
&quot;io&quot;
&quot;mime/multipart&quot;
&quot;net/http&quot;
&quot;os&quot;
)
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(&quot;Content-Type&quot;, 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 &lt;- err
return
}
in, err := os.Open(path)
if err != nil {
errchan &lt;- err
return
}
defer in.Close()
if written, err := io.Copy(w, in); err != nil {
errchan &lt;- fmt.Errorf(&quot;error copying %s (%d bytes written): %v&quot;, path, written, err)
return
}
if err := mwriter.Close(); err != nil {
errchan &lt;- err
return
}
}()
resp, err := client.Do(req)
merr := &lt;-errchan
if err != nil || merr != nil {
return resp, fmt.Errorf(&quot;http error: %v, multipart error: %v&quot;, err, merr)
}
return resp, nil
}

答案3

得分: 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("创建写入器时出错:%v", err)
	}
	if _, err = io.Copy(fw, file); err != nil {
		t.Errorf("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
}
// 不要忘记设置内容类型,它将包含边界。
req.Header.Set("Content-Type", w.FormDataContentType())
英文:

After having to decode the accepted answer for this question for use in my unit testing I finally ended up with the follow refactored code:

func createMultipartFormData(t *testing.T, fieldName, fileName string) (bytes.Buffer, *multipart.Writer) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&amp;b)
var fw io.Writer
file := mustOpen(fileName)
if fw, err = w.CreateFormFile(fieldName, file.Name()); err != nil {
t.Errorf(&quot;Error creating writer: %v&quot;, err)
}
if _, err = io.Copy(fw, file); err != nil {
t.Errorf(&quot;Error with io.Copy: %v&quot;, 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(&quot;PWD: &quot;, pwd)
panic(err)
}
return r
}

Now it should be pretty easy to use:

    b, w := createMultipartFormData(t, &quot;image&quot;,&quot;../luke.png&quot;)
req, err := http.NewRequest(&quot;POST&quot;, url, &amp;b)
if err != nil {
return
}
// Don&#39;t forget to set the content type, this will contain the boundary.
req.Header.Set(&quot;Content-Type&quot;, w.FormDataContentType())

答案4

得分: 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

英文:

Here is an option that works for files or strings:

package main
import (
&quot;bytes&quot;
&quot;io&quot;
&quot;mime/multipart&quot;
&quot;os&quot;
&quot;strings&quot;
)
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, &quot;@&quot;) {
val = val[1:]
file, err := os.Open(val)
if err != nil { return &quot;&quot;, nil, err }
defer file.Close()
part, err := mp.CreateFormFile(key, val)
if err != nil { return &quot;&quot;, nil, err }
io.Copy(part, file)
} else {
mp.WriteField(key, val)
}
}
return mp.FormDataContentType(), body, nil
}

Example:

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

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

答案5

得分: 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
}
英文:

Send file from one service to another:

func UploadFile(network, uri string, f multipart.File, h *multipart.FileHeader) error {
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
part, err := writer.CreateFormFile(&quot;file&quot;, 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(&quot;POST&quot;, uri, buf)
req.Header.Add(&quot;Content-Type&quot;, writer.FormDataContentType())
client := &amp;http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
b, _ = ioutil.ReadAll(resp.Body)
if resp.StatusCode &gt;= 400 {
return errors.New(string(b))
}
return nil
}

答案6

得分: -1

为了在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

Go代码:

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
}

这段代码可以在Go中执行一个带有POST请求的HTTP请求,并包含了一些额外的表单字段。

英文:

To extend on @attila-o answer, here is the code I went with to perform a POST HTTP req in Go with:

  • 1 file
  • configurable file name (f.Name() didn't work)
  • extra form fields.

Curl representation:

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

Go way:

client := &amp;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{
		&quot;owner&quot;: &quot;0xc916cfe5c83dd4fc3c3b0bf2ec2d4e401782875e&quot;,
		&quot;password&quot;: &quot;pwd&quot;,
	}

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

	req, err := http.NewRequest(&quot;POST&quot;, url, &amp;b)
	if err != nil {
		return nil, err
	}
	req.Header.Set(&quot;Content-Type&quot;, 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, &amp;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(&amp;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
}

</details>



# 答案7
**得分**: -4

我发现[这个教程][1]对于澄清我在Go中文件上传方面的困惑非常有帮助

基本上你可以通过在客户端使用`form-data`通过ajax上传文件并在服务器上使用以下小段Go代码

```go
file, handler, err := r.FormFile("img") // img是form-data的键
if err != nil {
    fmt.Println(err)
    return
}
defer file.Close()

fmt.Println("文件正常")
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.Request附注:这只是将文件存储在相同的文件夹中,并没有执行任何安全检查。

英文:

I have found this tutorial very helpful to clarify my confusions about file uploading in Go.

Basically you upload the file via ajax using form-data on a client and use the following small snippet of Go code on the server:

file, handler, err := r.FormFile(&quot;img&quot;) // img is the key of the form-data
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
fmt.Println(&quot;File is good&quot;)
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)

Here r is *http.Request. P.S. this just stores the file in the same folder and does not perform any security checks.

huangapple
  • 本文由 发表于 2013年11月26日 07:42:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/20205796.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定