Golang 多部分文件表单请求

标签 go curl

我正在针对 Mapbox 编写一个 API 客户端,将一批 svg 图像上传到自定义 map 。他们为此提供的 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
        }

        _, err = fw.Write(newBytes)

        if err != nil {
            log.Debugf("Could not write file to multipart: %v, %v", fileName, err)
            continue
        }
    }

    w.Close()

    return b, w
}

除了在实际请求中设置 header :
    bytes, formWriter := createMultipartFormData(filesMap)

    req, err := http.NewRequest("Post", fmt.Sprintf("https://api.mapbox.com/styles/v1/%v/%v/sprite?access_token=%v", "my_company", styleID, os.Getenv("MAPBOX_API_KEY")), &bytes)

    if err != nil {
        return err
    }

    req.Header.Set("User-Agent", "curl/7.64.1")
    req.Header.Set("Accept", "*/*")
    req.Header.Set("Content-Length", fmt.Sprintf("%v", len(bytes.Bytes())))
    req.Header.Set("Content-Type", formWriter.FormDataContentType())

    byts, _ := httputil.DumpRequest(req, true)
    fmt.Println(string(byts))

    res, err := http.DefaultClient.Do(req)

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

最佳答案

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

  • 他们经常错过某些有趣的细节。
  • 有时,他们会给出完全错误的建议。

  • 所以,这是一个可行的解决方案。
    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 发送它,我们也是如此。
  • 关于Golang 多部分文件表单请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63636454/

    相关文章:

    go - 如何让 golint 在 VS Code 上按类型运行而不是在保存时运行?

    go - 如何追踪 Go 模块中的依赖项来自何处?

    java - 如何在 Java 中执行等效的 cURL/mimetype 代码?

    c++ - 在 C++ 中使用 cURL 获取 JSON 文件,较大的文件没有出现或者可能太大

    go - GO标志无法正确解析

    Golang 函数 - 返回类型

    Go 锁定一片结构

    PHP 在 URL 中使用 cURL 和 GET 请求

    java - HttpURLConnection cURL GET 方法和 RequestProperty

    json - 获取 Twitch 中的直播用户列表