Golang 多部分文件表单请求

2023-12-01

我正在针对 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 执行相同操作时,我很快发现多形式库非常有限,并编写了一些代码来发出类似于上面提到的 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
}

除了在实际请求中设置标头之外:

    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.Writer and io.Reader因此可以很容易地与处理 I/O 的 Go stdlib 的其他部分组合。
  • 当准备发送多部分表单时,我们不会将整个文件的内容放入内存中,而是将它们直接“管道”到“多部分表单编写器”中。
  • 我们有一个查找表,它将要提交的文件名的扩展名映射到其 MIME 类型;我不知道 API 是否需要这个;如果不是真的需要,准备包含文件的表单字段的代码部分可以简化很多,但 cURL 会发送它,我们也是如此。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Golang 多部分文件表单请求 的相关文章

  • 从 freshdesk api 获取所有用户时获取curl_error(): 2 不是有效的 cURL 句柄资源

    我正在创建自己的系统来管理通过其 API 来自 freshdesk com 的所有票证 我正在发出curl 请求以从freshdesk com 获取数据 通过获取与股票相关的数据 它的工作正常 但是当我通过curl请求请求所有用户时 它会给
  • HAProxy SSL终止+客户端证书验证+curl/java客户端

    我希望使用我自己的自签名证书在 HAProxy 上进行 SSL 终止 并使用我创建的客户端证书验证客户端访问 我通过以下方式创建服务器 也是 CA 证书 openssl genrsa out ca key 1024 openssl req
  • 单值上下文中的多值错误

    我在编译 GO 代码时遇到此错误 multiple value fmt Println in single value context 我正在尝试创建一个函数 该函数接受可变数量的整数并将每个变量打印在一行上 GO package main
  • Golang 结构体初始化

    有一个像这样的简单结构 type Event struct Id int Name string 这两种初始化方法有什么区别呢 e1 Event Id 1 Name event 1 e2 Event Id 2 Name event 2 为什
  • 不要回显 cURL

    当我使用这段代码时 ch curl init url statuses curl exec ch curl close ch 我得到了我想要的东西 但如果我只是使用它 statuses被回显到页面上 我怎样才能阻止这个 将其放在第 2 行
  • 关闭长度未知的通道

    当不了解频道时我无法关闭频道 length package main import fmt time func gen ch chan int var i int for time Sleep time Millisecond 10 ch
  • Go 编译器有窗口化设置选项吗?

    我正在使用 Go 6g 编译 GTK 应用程序 我想知道是否有编译器 链接器选项使其成为 Windows 可执行文件而不是控制台可执行文件 MinGW 有一个 mwindows 选项来实现此目的 目前我必须使用十六进制编辑器手动更改 PE
  • 为什么authorize.net 使用的证书是由不在众所周知的curl.haxx.se/ca/cacert.pem 列表中的CA 签名的?

    与authorize net进行交易的URL是https secure authorize net gateway transact dll https secure authorize net gateway transact dll 如
  • 将中间件与 Golang Gorilla mux 子路由器结合使用

    如何将中间件应用到 Go 中大猩猩工具包 http www gorillatoolkit org 多路复用器子路由器 我有以下代码 router mux NewRouter StrictSlash true apiRouter router
  • 如何拥有在标准输出上更新的就地字符串

    我想输出到标准输出并让输出 覆盖 以前的输出 例如 如果我输出On 1 10 我想要下一个输出On 2 10覆盖On 1 10 我怎样才能做到这一点 stdout是一个流 io Writer 您无法修改已写入其中的内容 什么can更改的是该
  • 如何将长 Go 模板函数拆分为多行?

    我有一个很长的printf调用 Go 模板 例子 printf mongodb s s s s authSource admin replicaSet s readPreference nearest w majority Values r
  • 如何在golang中获得两个切片的交集?

    Go 中有没有有效的方法来获取两个切片的交集 我想避免嵌套 for 循环之类的解决方案slice1 string foo bar hello slice2 string foo bar intersection slice1 slice2
  • 如何在 Go 中获取给定月份的第一个星期一?

    我正在尝试获取给定月份的第一个星期一 我能想到的最好方法是循环前 7 天 然后返回 Weekday Monday 有一个更好的方法吗 通过查看时间的 Weekday 您可以计算出第一个星期一 package main import fmt
  • 如何构建一个不链接到 musl libc 的 go 可执行文件

    So 官方的 Go 构建容器基于 Alpine 高山用途musl https www musl libc org 作为 libc 而不是 glibc 我需要在容器中构建一个可以在使用 glibc 的 Ubuntu 上运行的 Go 可执行文件
  • 结构体到磁盘的高效 Go 序列化

    我的任务是将 C 代码替换为 Go 而且我对 Go API 还很陌生 我正在使用 gob 将数百个键 值条目编码到磁盘页面 但 gob 编码有太多不需要的膨胀 package main import bytes encoding gob f
  • Golang:如何在HTTP客户端的TLS配置中指定证书

    我有一个证书文件 该位置是 usr abc my crt我想将该证书用于我的 tls 配置 以便我的 http 客户端在与其他服务器通信时使用该证书 我当前的代码如下 mTLSConfig tls Config CipherSuites u
  • []interface{}{} 中的双大括号是什么意思

    请注意 这是格式为 的双花括号 而不是嵌套花括号 我也不确定这是空接口问题 切片问题还是结构问题 我猜它至少是其中两个的组合 我正在学习 Golang 并且已经达到了空接口 我发现我需要将空接口声明为 interface 或者例如 inte
  • 在 golang 中将 []uint32 转换为 []byte,反之亦然

    最有效的 性能 转换方式是什么 uint32向和从 byte在戈兰 例如 func main source uint32 1 2 3 dest make byte 4 len source source to dest check len
  • Golang http 写入响应,无需等待完成

    我正在构建一个应用程序 该应用程序构建 pdf 文件并在收到请求时将其返回给客户端 由于其中一些 pdf 文件可能需要一些时间才能生成 我想在客户端运行时定期将某种状态更新发送回客户端 当完成 pdf 文件的构建后 它也应该返回给客户 类似
  • 在 Go 中解析多个 JSON 对象

    可以使用以下方法轻松解析如下对象encoding json包裹 something foo something else bar 我面临的问题是当服务器返回多个字典时 如下所示 something foo something else ba

随机推荐

  • R:从 5 个元素组合的数据框中提取内部更高级别的组合(1、2、3 和 4 个元素的组)

    抱歉 我必须跟进另一个问题this one and 另一个 虽然第二个问题的答案完美地解决了 MWE 但在我的现实世界数据中 我需要以不同的方式做事 并且想知道是否有人可以提供帮助 所以这一次 我的起点是一个数据框 名为plusminus
  • Android 应用程序无法在模拟器上启动

    我只是按照这个设置eclipse开始android开发http developer android com sdk installing html 我的问题似乎与此类似 Android 应用程序无法在模拟器上启动 但该解决方案不起作用 我正
  • 使用 jackson 反序列化包含 @JsonFormat(shape=JsonFormat.Shape.ARRAY) 和自定义对象的 json

    我有一个自定义对象 public class Response JsonProperty values private List
  • 如何通过 Java 在 SSH 中运行多个命令?

    如何使用 Java 运行时在 SSH 中运行多个命令 使用较新的流程构建器类而不是Runtime exec 您可以通过指定程序及其参数列表来构造一个程序 如下面我的代码所示 您不需要在命令周围使用单引号 您还应该阅读 stdout 和 st
  • 检测链接是否已被单击,如果已单击则应用不同的 CSS 类

    所以我想做的是确定当前链接是否处于 活动 状态 即用户是否刚刚单击了该链接 如果是的话 那么我想应用class selected对此 li item 这就是我正在处理的事情 ul li a href Link 1 a li li a hre
  • ExtJS 4 / Sencha Touch 容器、组件、元素和面板

    Container Component Element Panel之间有何联系或区别 请帮忙 如果短暂 Element是 DOM 元素的包装器 成分是所有小部件的基本类 容器是组件的子类 它可以有 项目 即容器可以包含其他组件 Panel是
  • S3分块上传异常

    使用 S3 MultiPartAPI 我得到一个异常 您建议的上传内容小于允许的最小大小 我用 11MB 和 7MB 的 2 个文件进行了测试 1MB 就可以正常工作 FileStream fs new FileStream foo Fil
  • Django基于添加表单编辑表单?

    我制作了一个漂亮的表单 以及一个用于处理它的大而复杂的 添加 函数 事情是这样开始的 def add req if req method POST form ArticleForm req POST if form is valid art
  • 我们可以与 psql 脚本交互吗?

    我们可以做类似的事情吗 echo Type username to show its properties SELECT FROM mY users WHERE username echo End of script 在 psql 脚本文件
  • mysql 获取字符串的 ascii 代码转储

    在MySQL中 有没有一种简单的方法SELECT获取 a 中每个字符的 ASCII 代码 代码点序列varchar价值 我比较熟悉Oracle 它有DUMP可以用于此目的的函数 例如 select some function abcd 会返
  • 在 HTML 选项卡条中创建选定的选项卡

    2022 年 2 月 17 日 我花了一段时间 但我弄清楚发生了什么事 每个 TD 都有一个锚点 A 标签 并且 TD 和锚点都有类别 锚点只有一个类 但它是最后应用的 因此 TD 显然将该类作为其类列表的一部分 因此 当前类将应用于锚标记
  • 无法解析 styles.xml 中的符号“主题”(Android Studio)

    从今天起 Android Studio 无法在 styles xml 中找到 AppCompat 主题 但例如代码中的 AppCompatActivity 确实可以识别 我的 Android Studio 版本是 2 2 2 Build A
  • 具有单个全局变量的 cpp 文件将被忽略

    我正在尝试初始化全局地图 std map
  • awk 根据列合并行

    想要根据第一列合并行 1到行并格式化输出 打印时需要生成headerMax Unique count of first field 例如 安哥拉显示 count 3 巴西显示 count 5 赞比亚显示 count 1 字段 1 的最大唯一
  • NginX 友好的 PHP 框架

    我正在寻找一种 PHP 框架 如果幸运的话 它只能在 FastCGI 下的 nginx 中工作 否则 不需要太多调整 Symfony 1 4 与 nginx 非常棒 我已经完成了调整 这是我的生产配置的概括 我可以保证它适合生产使用 ser
  • 删除因子级别的“空单元格”

    我有一个数据框 其中有一列 列中有一些数据和一些空单元格 当我检查该列的级别时 它显示三个级别 因为它将空单元格作为一个级别 我想删除那个级别 假设我有 editor note starting from R 4 0 0 stringsAs
  • HTML输入24位格式的时间

    我正在使用下面的 HTML 标签
  • 黑莓条码扫描库?

    有人很好地掌握了条形码扫描库 可以根据数码相机的输入读取 UPC A EAN 13 或其他主要条形码格式吗 RIM 是否有可用于此的标准库 我知道 BlackBerry Messenger 内置了 2D 条码扫描功能 所以我猜测一定有可用的
  • 将上下文菜单添加到 Inno Setup 页面

    如何将一些上下文菜单添加到 Inno Setup 的特定页面 例如 在安装页面中 如果用户右键单击页面 他可以看到 取消 或 暂停 菜单项 可以执行一些操作 Inno Setup 没有上下文菜单 API 甚至没有用于处理鼠标点击的 API
  • Golang 多部分文件表单请求

    我正在针对 Mapbox 编写一个 API 客户端 将一批 svg 图像上传到自定义地图 他们为此提供的 api 记录了一个运行良好的 cUrl 调用示例 curl F images include mapbox sprites dark