如何构建有效的 ActiveStorage 直接上传请求到 GCS?

How to build a valid ActiveStorage direct upload request to GCS?

这是关于将 ActiveStorage 与 GCS 一起用于 API 客户端用例。 (Rails 5.2.1, 5.2.2)

我正在编写一个测试来探索如何制作一个模拟直接上传到 GCS 的请求,该请求由通用 DirectUploadsController 准备。 This generic controller 是 ActiveStorage 的一部分。这个想法是稍后在与同一后端对话的移动应用程序中复制代码。

AS 配置在开发环境中运行良好,既可以使用通过控制器上传,也可以使用 AS 附带的 JS 集成直接上传。这就是为什么我认为配置一定没问题。 ('test' 和 'development' env 在这个阶段使用完全相同的设置。)

测试代码在下框内

它总是从 RestClient.put 调用中引发 403 Forbidden 响应。

响应消息抱怨签名不匹配,详情如下。先上测试代码:

require 'test_helper'

class UploadControllerTest < ActionDispatch::IntegrationTest
  test "direct upload from controller prepared blob" do
    pathname = file_fixture('cube.png')
    data = pathname.binread
    content_type = "image/png"

    post rails_direct_uploads_path, params: {
      blob: {
        filename: pathname.basename,
        byte_size: pathname.size,
        checksum: Digest::MD5.base64digest(data),
        content_type: content_type
      }
    }

    assert_equal 27195, pathname.size
    assert_response :success

    json = response.parsed_body
    direct_upload = json["direct_upload"]
    signed_url    = direct_upload["url"]
    headers       = direct_upload["headers"]

    assert_equal({ "Content-MD5" => "tmBHZQCm+qBzGFEaDwmpnA==" }, headers)

    assert_match /&Signature=/, signed_url
    assert_match /&Expires=/, signed_url
    assert_match %r{^https://storage.googleapis.com}, signed_url

    response = RestClient.put(
      signed_url,
      data,
      headers.merge("Content-Type" => content_type)
    )

    assert_response :success
  rescue RestClient::Forbidden => e
    pp e.response.body
    fail "Failing with 403 Forbidden" # always ends up here
  end
end

生成的响应正文是这样的 XML:

  <?xml version='1.0' encoding='UTF-8'?>
  <Error>
    <Code>SignatureDoesNotMatch</Code>
    <Message>
      The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.
    </Message>
    <StringToSign>PUT\n" +
    "tmBHZQCm+qBzGFEaDwmpnA==\n" +
    "image/png\n" +
    "1544517548\n" +
  "/planprop-test-bucket/gVn9zVCumGJxiu2kU6mFWUVV</StringToSign>
  </Error>

错误代码为:

SignatureDoesNotMatch

以及随附的消息:

The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.

签名字符串的列出部分是校验和(上面断言)、过期时间(URL的一部分)、内容类型(上面断言)和对象(桶名和密钥,部分URL)。所以我没有看到可能会出现不匹配的部分。

有什么问题吗?

上面代码的问题是发送了application/x-www-form-urlencodedContent-Typeheader,而签名时使用了none

要使其正常工作,请将 PUT 请求代码更改为

response = RestClient.put(
  signed_url,
  data,
  headers.merge(content_type: "")
)

使用 nil 仍会在请求中显示此默认值。

顺便说一下,这种行为并不是 RestClient 独有的。但在我测试的许多 Ruby 个 http 客户端(Faraday、net/http、httpclient)中也是如此。 Excon 在这里是个例外,它不会在没有被告知的情况下发送默认内容类型 header。