Golang 多部分文件表单请求

我正在针对 Mapbox 编写一个 API 客户端,将一批 svg 图像上传到自定义地图。他们为此提供的 api 记录在一个可以正常工作的示例 cUrl 调用中:


curl -F images=@include/mapbox/sprites_dark/aubergine_selected.svg "https://api.mapbox.com/styles/v1/<my_company>/<my_style_id>/sprite?access_token=$MAPBOX_API_KEY" --trace-ascii /dev/stdout


当尝试从 golang 做同样的事情时,我很快发现 multiform 库非常有限,并编写了一些代码来发出类似于上面提到的 cUrl 请求的请求。


func createMultipartFormData(fileMap map[string]string) (bytes.Buffer, *multipart.Writer) {

    var b bytes.Buffer

    var err error

    w := multipart.NewWriter(&b)

    var fw io.Writer

    for fileName, filePath := range fileMap {


        h := make(textproto.MIMEHeader)

        h.Set("Content-Disposition",

            fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "images", fileName))

        h.Set("Content-Type", "image/svg+xml")


        if fw, err = w.CreatePart(h); err != nil {

            fmt.Printf("Error creating form File %v, %v", fileName, err)

            continue

        }


        fileContents, err := ioutil.ReadFile(filePath)

        fileContents = bytes.ReplaceAll(fileContents, []byte("\n"), []byte("."))


        blockSize := 64

        remainder := len(fileContents) % blockSize

        iterations := (len(fileContents) - remainder) / blockSize


        newBytes := []byte{}

        for i := 0; i < iterations; i++ {

            start := i * blockSize

            end := i*blockSize + blockSize

            newBytes = append(newBytes, fileContents[start:end]...)

            newBytes = append(newBytes, []byte("\n")...)

        }


        if remainder > 0 {

            newBytes = append(newBytes, fileContents[iterations*blockSize:]...)

            newBytes = append(newBytes, []byte("\n")...)

        }


        if err != nil {

            fmt.Printf("Error reading svg file: %v: %v", filePath, err)

            continue

        }


甚至想尽可能限制行长并复制 cUrl 使用的编码,但到目前为止还没有运气。有经验的人知道为什么这适用于 cUrl 而不是 golang?


胡子哥哥
浏览 137回答 2
2回答

慕容森

好吧,我承认解决您的任务的“拼图”的所有部分都可以在网上找到,这有两个问题:他们经常错过某些有趣的细节。有时,他们会给出完全错误的建议。所以,这是一个可行的解决方案。package mainimport (&nbsp; &nbsp; "bytes"&nbsp; &nbsp; "fmt"&nbsp; &nbsp; "io"&nbsp; &nbsp; "io/ioutil"&nbsp; &nbsp; "mime"&nbsp; &nbsp; "mime/multipart"&nbsp; &nbsp; "net/http"&nbsp; &nbsp; "net/textproto"&nbsp; &nbsp; "net/url"&nbsp; &nbsp; "os"&nbsp; &nbsp; "path/filepath"&nbsp; &nbsp; "strconv"&nbsp; &nbsp; "strings")func main() {&nbsp; &nbsp; const (&nbsp; &nbsp; &nbsp; &nbsp; dst&nbsp; &nbsp;= "https://api.mapbox.com/styles/v1/AcmeInc/Style_001/sprite"&nbsp; &nbsp; &nbsp; &nbsp; fname = "path/to/a/sprite/image.svg"&nbsp; &nbsp; &nbsp; &nbsp; token = "an_invalid_token"&nbsp; &nbsp; )&nbsp; &nbsp; err := post(dst, fname, token)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; fmt.Fprintln(os.Stderr, err)&nbsp; &nbsp; &nbsp; &nbsp; os.Exit(1)&nbsp; &nbsp; }}func post(dst, fname, token string) error {&nbsp; &nbsp; u, err := url.Parse(dst)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return fmt.Errorf("failed to parse destination url: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; form, err := makeRequestBody(fname)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return fmt.Errorf("failed to prepare request body: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; q := u.Query()&nbsp; &nbsp; q.Set("access_token", token)&nbsp; &nbsp; u.RawQuery = q.Encode()&nbsp; &nbsp; hdr := make(http.Header)&nbsp; &nbsp; hdr.Set("Content-Type", form.contentType)&nbsp; &nbsp; req := http.Request{&nbsp; &nbsp; &nbsp; &nbsp; Method:&nbsp; &nbsp; &nbsp; &nbsp; "POST",&nbsp; &nbsp; &nbsp; &nbsp; URL:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;u,&nbsp; &nbsp; &nbsp; &nbsp; Header:&nbsp; &nbsp; &nbsp; &nbsp; hdr,&nbsp; &nbsp; &nbsp; &nbsp; Body:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ioutil.NopCloser(form.body),&nbsp; &nbsp; &nbsp; &nbsp; ContentLength: int64(form.contentLen),&nbsp; &nbsp; }&nbsp; &nbsp; resp, err := http.DefaultClient.Do(&req)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return fmt.Errorf("failed to perform http request: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; defer resp.Body.Close()&nbsp; &nbsp; _, _ = io.Copy(os.Stdout, resp.Body)&nbsp; &nbsp; return nil}type form struct {&nbsp; &nbsp; body&nbsp; &nbsp; &nbsp; &nbsp; *bytes.Buffer&nbsp; &nbsp; contentType string&nbsp; &nbsp; contentLen&nbsp; int}func makeRequestBody(fname string) (form, error) {&nbsp; &nbsp; ct, err := getImageContentType(fname)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; `failed to get content type for image file "%s": %w`,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fname, err)&nbsp; &nbsp; }&nbsp; &nbsp; fd, err := os.Open(fname)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf("failed to open file to upload: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; defer fd.Close()&nbsp; &nbsp; stat, err := fd.Stat()&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf("failed to query file info: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; hdr := make(textproto.MIMEHeader)&nbsp; &nbsp; cd := mime.FormatMediaType("form-data", map[string]string{&nbsp; &nbsp; &nbsp; &nbsp; "name":&nbsp; &nbsp; &nbsp;"images",&nbsp; &nbsp; &nbsp; &nbsp; "filename": fname,&nbsp; &nbsp; })&nbsp; &nbsp; hdr.Set("Content-Disposition", cd)&nbsp; &nbsp; hdr.Set("Contnt-Type", ct)&nbsp; &nbsp; hdr.Set("Content-Length", strconv.FormatInt(stat.Size(), 10))&nbsp; &nbsp; var buf bytes.Buffer&nbsp; &nbsp; mw := multipart.NewWriter(&buf)&nbsp; &nbsp; part, err := mw.CreatePart(hdr)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf("failed to create new form part: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; n, err := io.Copy(part, fd)&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf("failed to write form part: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; if int64(n) != stat.Size() {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf("file size changed while writing: %s", fd.Name())&nbsp; &nbsp; }&nbsp; &nbsp; err = mw.Close()&nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; return form{}, fmt.Errorf("failed to prepare form: %w", err)&nbsp; &nbsp; }&nbsp; &nbsp; return form{&nbsp; &nbsp; &nbsp; &nbsp; body:&nbsp; &nbsp; &nbsp; &nbsp; &buf,&nbsp; &nbsp; &nbsp; &nbsp; contentType: mw.FormDataContentType(),&nbsp; &nbsp; &nbsp; &nbsp; contentLen:&nbsp; buf.Len(),&nbsp; &nbsp; }, nil}var imageContentTypes = map[string]string{&nbsp; &nbsp; "png":&nbsp; "image/png",&nbsp; &nbsp; "jpg":&nbsp; "image/jpeg",&nbsp; &nbsp; "jpeg": "image/jpeg",&nbsp; &nbsp; "svg":&nbsp; "image/svg+xml",}func getImageContentType(fname string) (string, error) {&nbsp; &nbsp; ext := filepath.Ext(fname)&nbsp; &nbsp; if ext == "" {&nbsp; &nbsp; &nbsp; &nbsp; return "", fmt.Errorf("file name has no extension: %s", fname)&nbsp; &nbsp; }&nbsp; &nbsp; ext = strings.ToLower(ext[1:])&nbsp; &nbsp; ct, found := imageContentTypes[ext]&nbsp; &nbsp; if !found {&nbsp; &nbsp; &nbsp; &nbsp; return "", fmt.Errorf("unknown file name extension: %s", ext)&nbsp; &nbsp; }&nbsp; &nbsp; return ct, nil}一些关于实现的随机注释可帮助您理解这些概念:为了构造请求的有效负载(正文),我们使用了一个bytes.Buffer实例。它有一个很好的属性,指向它的指针 (&nbsp;*bytes.Buffer) 实现了两者io.Writer,io.Reader因此可以很容易地与处理 I/O 的 Go 标准库的其他部分组合。在准备要发送的多部分表单时,我们不会将整个文件的内容吞入内存,而是将它们直接“管道”到“多部分表单编写器”中。我们有一个查找表,它将要提交的文件名的扩展名映射到其 MIME 类型;我不知道 API 是否需要这样做;如果不是真的需要,准备包含文件的表单字段的代码部分可以简化很多,但是 cURL 发送它,我们也是如此。

MMTTMM

只是好奇,这是为了什么?&nbsp; &nbsp; &nbsp; &nbsp; fileContents = bytes.ReplaceAll(fileContents, []byte("\n"), []byte("."))&nbsp; &nbsp; &nbsp; &nbsp; blockSize := 64&nbsp; &nbsp; &nbsp; &nbsp; remainder := len(fileContents) % blockSize&nbsp; &nbsp; &nbsp; &nbsp; iterations := (len(fileContents) - remainder) / blockSize&nbsp; &nbsp; &nbsp; &nbsp; newBytes := []byte{}&nbsp; &nbsp; &nbsp; &nbsp; for i := 0; i < iterations; i++ {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; start := i * blockSize&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; end := i*blockSize + blockSize&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBytes = append(newBytes, fileContents[start:end]...)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBytes = append(newBytes, []byte("\n")...)&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if remainder > 0 {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBytes = append(newBytes, fileContents[iterations*blockSize:]...)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBytes = append(newBytes, []byte("\n")...)&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; if err != nil {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fmt.Printf("Error reading svg file: %v: %v", filePath, err)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; continue&nbsp; &nbsp; &nbsp; &nbsp; }将整个文件读入内存很少是一个好主意(ioutil.ReadFile)。正如@muffin-top 所说,那三行代码怎么样?&nbsp; &nbsp; for fileName, filePath := range fileMap {&nbsp; &nbsp; &nbsp; &nbsp; // h := ...&nbsp; &nbsp; &nbsp; &nbsp; fw, _ := w.CreatePart(h) // TODO: handle error&nbsp; &nbsp; &nbsp; &nbsp; f, _ := os.Open(filePath) // TODO: handle error&nbsp; &nbsp; &nbsp; &nbsp; io.Copy(fw, f) // TODO: handle error&nbsp; &nbsp; &nbsp; &nbsp; f.Close() // TODO: handle error&nbsp; &nbsp; }
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go