这篇文章简单谈谈在ActiveStorage的加持下OSS服务的直连上传模式,依旧是使用阿里云的OSS作为例子,想必其他服务商的OSS也是大同小异。

用武之地

其实Rails的ActiveStorage再搭配各种第三方的插件,已经让Rails跟各大厂商的OSS服务能够无缝衔接了。正常来说只要简单改改配置,就能够让自家的服务切换到别家的OSS服务商,且不用改动任何的业务代码。

而且在业务逻辑里面如果需要做文件上传,那么可以很简单地编写出以下接口

# frozen_string_literal: true

module Api
  module V1
    class UtilsController < ApplicationController
      def file_uploads
        raise Exceptions::InvalidParams, '非法参数' unless params[:file].is_a?(ActionDispatch::Http::UploadedFile)

        blob = ActiveStorage::Blob.create_after_upload!(io: params[:file], filename: params[:file].original_filename) # 统一接口
        json_response({
                        signed_id: blob.signed_id,
                        url: blob.service_url
                      }, :created)
      end
    end
  end
end

这种做法的好处在于编码简单。不用理会太多加密解密的事情,客户端直接通过file参数传输需要上传的文件即可。然而,所有文件上传都经过Rails服务接口,会有以下问题

  1. 当上传文件较大,或者频次较高的时候自家服务器负载较高,影响正常的业务逻辑。
  2. 上传时间较长,因为文件需要传输两次。第一次传输到Rails服务,第二次从Rails服务上传到OSS服务。当遇到文件较大的场景,请求会长时间得不到响应。

这种场景下,直连上传功能就有用武之地了,相当于用户直接往OSS服务上送文件。完美解决前面提到的两个问题。

PS:直连上传的前提是OSS服务处于私有读写/只读的状态下。直接把OSS服务弄成公共读写也能很简单地实现直连上传,然而安全性就有待考量了,这篇文章便失去了意义。

直连上传秘籍

这里依旧使用阿里云的OSS服务来作为教材。既然要对私有化的OSS服务做直连上传,那么就必须要附带签名信息才行。否则的话上传会出现问题

Screen Shot 2022-07-21 at 09.35.57.png

可见,直接上传的话会受到权限的制约。要如何解决这个问题呢?ActiveStorage做了个不错的解决方案,当然还是要依赖适配器。在Rails搭配ActiveStorage的场景下每个ActiveStorage::Blob资源其实会对应OSS上的一个资源坑位,我们只要往这个坑位上上传文件即可。可以分几步走

占坑位

占坑位其实很简单,创建一个ActiveStorage::Blob资源就好了,假设我们要上传一个mp4文件,大小

> ls -la example.png
-rw-r--r--@ 1 lan  staff  131267 Jul 15 11:00 example.png

大概是131267个字节。对其md5并进行base64编码后可得到

> openssl dgst -md5 -binary ~/Desktop/example.png | base64
ZNRYpBB1Wz20ix5jwjFq3Q==

这两个都是占取坑位的重要参数,应该在进行直连上传之前通过客户端软件计算得出,并用以下的方式来占取坑位

blob = ActiveStorage::Blob.create(filename: 'example.png', byte_size: 131267, checksum: 'ZNRYpBB1Wz20ix5jwjFq3Q==', content_type: 'image/png')

接下来就可以利用ActiveStorage::Blob的特性,获取到往该坑位推送资源的必要参数了

  • 上传URL
  • 头部签名

往坑位推送信息

获取上传链接

> blob.service_url_for_direct_upload
=> "https://huiliu-staging.oss-cn-beijing.aliyuncs.com/resources/3bdec2dqnt5altpsjj4es5fxx9we"

获取上传所需要的头部信息

> blob.service_headers_for_direct_upload
=> {"Content-Type"=>"image/png", "Content-MD5"=>"ZNRYpBB1Wz20ix5jwjFq3Q==", "Authorization"=>"OSS LTAI4G6YWffqEULUZEjFVHMM:tPA0XdgC1xnJIJa0s4pvTNKZOzw=", "x-oss-date"=>"Fri, 22 Jul 2022 23:11:13 GMT"}

头部信息只在一定的时间内有效,因此可以在一定程度上保证上传的安全性。接着便可以利用上述信息往对应的坑位推送资源了,我且在Postman上完成这个过程,选择文件,填写头部参数并点击发送,即返回200响应

Screen Shot 2022-07-23 at 07.14.52.png

Screen Shot 2022-07-23 at 07.31.52.png

我们可以通过service_url来获取对应的访问链接,并进行访问

> blob.service_url
=> "https://huiliu-staging-resources.huiliu.net/resources/3bdec2dqnt5altpsjj4es5fxx9we"

Screen Shot 2022-07-27 at 07.09.33.png

真实业务

把上面的概念应用到实际业务中,一般是要分几步走

  1. 客户端计算出文件的大小(单位为字节)以及md5的值,并对md5的值进行base64处理。(注:这里大小不是必须的,传入大小只是为了记录元数据。MD5的值也不是必须的,如果不想做文件完整性校验可以直接传入空字符串。)
  2. 调用Rails服务接口占取坑位,并获得直连上传的链接以及上传所需的头部信息。
  3. 通过直连接口上传对应的文件。

Rails服务用来占取坑位的接口大概长下面这个样子。

module Api
  module V1
    class UtilsController < ApplicationController
      def upload_directly_meta_information
        blob = ActiveStorage::Blob.create(filename: params[:filename].to_s, byte_size: params[:byte_size], checksum: params[:checksum].to_s, content_type: params[:content_type].to_s)

        json_response({
                        url: blob.service_url_for_direct_upload,
                        headers: blob.service_headers_for_direct_upload,
                        signed_id: blob.signed_id
                      })
      end
    end
  end
end

客户端只要用相应的元信息调用接口之后便能得到对应的上传连接url,以及上传所需的头部信息headers。这里还会返回signed_id,主要是为了上传成功之后,客户端可以利用这个signed_id把资源与某个数据表通过ActiveStorage::Attachment进行关联。

总结

这篇文章简单总结了在Rails搭配ActiveStorage使用的场景下要如何进行直连文件上传。一般要分几步走,这种做法虽然繁琐些但性能较好,且不会给自家服务带来太大的压力,针对大文件上传以及频次较高的文件上传等场景十分有效。