为了账号安全,请及时绑定邮箱和手机立即绑定

Golang 多部分文件表单请求

Golang 多部分文件表单请求

Go
胡子哥哥 2022-06-27 15:09:48
我正在针对 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?
查看完整描述

2 回答

?
慕容森

TA贡献1853条经验 获得超18个赞

好吧,我承认解决您的任务的“拼图”的所有部分都可以在网上找到,这有两个问题:

  • 他们经常错过某些有趣的细节。

  • 有时,他们会给出完全错误的建议。

所以,这是一个可行的解决方案。

package main


import (

    "bytes"

    "fmt"

    "io"

    "io/ioutil"

    "mime"

    "mime/multipart"

    "net/http"

    "net/textproto"

    "net/url"

    "os"

    "path/filepath"

    "strconv"

    "strings"

)


func main() {

    const (

        dst   = "https://api.mapbox.com/styles/v1/AcmeInc/Style_001/sprite"

        fname = "path/to/a/sprite/image.svg"

        token = "an_invalid_token"

    )


    err := post(dst, fname, token)

    if err != nil {

        fmt.Fprintln(os.Stderr, err)

        os.Exit(1)

    }

}


func post(dst, fname, token string) error {

    u, err := url.Parse(dst)

    if err != nil {

        return fmt.Errorf("failed to parse destination url: %w", err)

    }


    form, err := makeRequestBody(fname)

    if err != nil {

        return fmt.Errorf("failed to prepare request body: %w", err)

    }


    q := u.Query()

    q.Set("access_token", token)

    u.RawQuery = q.Encode()


    hdr := make(http.Header)

    hdr.Set("Content-Type", form.contentType)

    req := http.Request{

        Method:        "POST",

        URL:           u,

        Header:        hdr,

        Body:          ioutil.NopCloser(form.body),

        ContentLength: int64(form.contentLen),

    }


    resp, err := http.DefaultClient.Do(&req)

    if err != nil {

        return fmt.Errorf("failed to perform http request: %w", err)

    }

    defer resp.Body.Close()


    _, _ = io.Copy(os.Stdout, resp.Body)


    return nil

}


type form struct {

    body        *bytes.Buffer

    contentType string

    contentLen  int

}


func makeRequestBody(fname string) (form, error) {

    ct, err := getImageContentType(fname)

    if err != nil {

        return form{}, fmt.Errorf(

            `failed to get content type for image file "%s": %w`,

            fname, err)

    }


    fd, err := os.Open(fname)

    if err != nil {

        return form{}, fmt.Errorf("failed to open file to upload: %w", err)

    }

    defer fd.Close()


    stat, err := fd.Stat()

    if err != nil {

        return form{}, fmt.Errorf("failed to query file info: %w", err)

    }


    hdr := make(textproto.MIMEHeader)

    cd := mime.FormatMediaType("form-data", map[string]string{

        "name":     "images",

        "filename": fname,

    })

    hdr.Set("Content-Disposition", cd)

    hdr.Set("Contnt-Type", ct)

    hdr.Set("Content-Length", strconv.FormatInt(stat.Size(), 10))


    var buf bytes.Buffer

    mw := multipart.NewWriter(&buf)


    part, err := mw.CreatePart(hdr)

    if err != nil {

        return form{}, fmt.Errorf("failed to create new form part: %w", err)

    }


    n, err := io.Copy(part, fd)

    if err != nil {

        return form{}, fmt.Errorf("failed to write form part: %w", err)

    }


    if int64(n) != stat.Size() {

        return form{}, fmt.Errorf("file size changed while writing: %s", fd.Name())

    }


    err = mw.Close()

    if err != nil {

        return form{}, fmt.Errorf("failed to prepare form: %w", err)

    }


    return form{

        body:        &buf,

        contentType: mw.FormDataContentType(),

        contentLen:  buf.Len(),

    }, nil

}


var imageContentTypes = map[string]string{

    "png":  "image/png",

    "jpg":  "image/jpeg",

    "jpeg": "image/jpeg",

    "svg":  "image/svg+xml",

}


func getImageContentType(fname string) (string, error) {

    ext := filepath.Ext(fname)

    if ext == "" {

        return "", fmt.Errorf("file name has no extension: %s", fname)

    }


    ext = strings.ToLower(ext[1:])

    ct, found := imageContentTypes[ext]

    if !found {

        return "", fmt.Errorf("unknown file name extension: %s", ext)

    }


    return ct, nil

}

一些关于实现的随机注释可帮助您理解这些概念:

  • 为了构造请求的有效负载(正文),我们使用了一个bytes.Buffer实例。
    它有一个很好的属性,指向它的指针 ( *bytes.Buffer) 实现了两者io.Writerio.Reader因此可以很容易地与处理 I/O 的 Go 标准库的其他部分组合。

  • 在准备要发送的多部分表单时,我们不会将整个文件的内容吞入内存,而是将它们直接“管道”到“多部分表单编写器”中。

  • 我们有一个查找表,它将要提交的文件名的扩展名映射到其 MIME 类型;我不知道 API 是否需要这样做;如果不是真的需要,准备包含文件的表单字段的代码部分可以简化很多,但是 cURL 发送它,我们也是如此。


查看完整回答
反对 回复 2022-06-27
?
MMTTMM

TA贡献1869条经验 获得超4个赞

只是好奇,这是为了什么?


        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

        }

将整个文件读入内存很少是一个好主意(ioutil.ReadFile)。


正如@muffin-top 所说,那三行代码怎么样?


    for fileName, filePath := range fileMap {


        // h := ...


        fw, _ := w.CreatePart(h) // TODO: handle error


        f, _ := os.Open(filePath) // TODO: handle error


        io.Copy(fw, f) // TODO: handle error


        f.Close() // TODO: handle error

    }


查看完整回答
反对 回复 2022-06-27
  • 2 回答
  • 0 关注
  • 135 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信