使用ActiveStorage记录

ActiveStorage使用记录。

写在前面

activeStorage是Rails5.2 的新功能,官方给到的定义是:

Active Storage facilitates uploading files to a cloud storage service like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those files to Active Record objects. It comes with a local disk-based service for development and testing and supports mirroring files to subordinate services for backups and migrations.

开发及测试环境下,文件会默认存储在本地磁盘,生产环境下,可将文件直接上传到云端,比如Amazon S3, Google Cloud Storage, 或 Microsoft Azure Storage,同时会将这些文件关联到对应的 activeRecord对象。

这里结合自己最近踩的坑,记录下ActiveStorage的使用,主要涉及基本的用法,相关表结构及数据迁移部分。

使用

  • 生成表active_storage_blobs和active_storage_attachments

    rails5.2自带ActiveStorage,终端直接执行:

    rails active_storage:install
    

    随后会生成一个migration文件,文件内容长这样:

    class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
      def change
        create_table :active_storage_blobs do |t|
          t.string   :key,        null: false
          t.string   :filename,   null: false
          t.string   :content_type
          t.text     :metadata
          t.bigint   :byte_size,  null: false
          t.string   :checksum,   null: false
          t.datetime :created_at, null: false
    
          t.index [ :key ], unique: true
        end
    
        create_table :active_storage_attachments do |t|
          t.string     :name,     null: false
          t.references :record,   null: false, polymorphic: true, index: false
          t.references :blob,     null: false
    
          t.datetime :created_at, null: false
    
          t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
        end
      end
    end
    

    终端执行:

    rake db:migrate
    

    这会生成两张表,active_storage_blobs 和active_storage_attachments。这两张表很关键,后面会说到文件的存储与提取。

  • 设置存储路径

    在config/storage.yml 进行配置:

    默认配置如下:

    • 开发:service是磁盘disk,上传的文件存放在storage中,你会发现新建一个rails5.2项目时,根目录下会自动生成一个storage文件夹,就是开发时上传文件的存储位置。
    • 测试:service是磁盘disk,文件放在tmp/storage下
    • 生产:service有三个云存储可选,Amazon S3, Google Cloud Storage, 或 Microsoft Azure Storage, 自己设置bucket即可。

    在config/environments下,可以看到三种环境对应的Rails active_storage的默认存储路径。

    ## 开发
    config.active_storage.service = :local
    ## 生产
    config.active_storage.service = :local
    ## 测试
    config.active_storage.service = :test
    
  • 单文件上传

    假定现在有一个user model,每一个user有一个avatar,用户上传avatar。

    传统做法是安装gem carrierwave, 然后rails g uploader Avatar,生成继承自CarrierWave::Uploader::Base的AvatarUploader,然后在user.rb中,添加mount_uploader :avatar, AvatarUploader即可。

    而使用ActiveStorage,只需要在user.rb中,添加如下语句即可:

    has_one_attached :avatar
    

    显示的使用,文件的URL通过url_for(avatar)来获取.

  • 多文件上传

    比如一个商品有多张图片pictures,在product.rb中,添加:

    has_many_attached :pictures
    

    在对应的controller中,对于params部分,声明pictures是[]:

    def product_params
      params.require(:product).permit(:title, :xxxx, :xxxx, pictures: [])
    end
    
  • 数据的存储与文件的提取

    以一个user的头像avatar为例,看avatar是如何存入的。

    上面提到会生成两张表,active_storage_blobs 和active_storage_attachments。其中上传的文件信息会先存储在active_storage_blobs中,随后在 active_storage_blobs中写入新的记录,active_storage_blobs主要用于将上传的文件与对应的model ID建立关联。而通过:record_type, :record_id, :name, :blob_id可以定位到唯一的上传文件。

    可以进入rails console,通过ActiveStorage::Blob,ActiveStorage::Attachment来进行数据的增删改查等。

    以model User为例,看两条record感受一下:

     ### ActiveStorage::Blob.first
     id: 1,
     key: "imaQysUeeTaW1xTEVzWHyEFP",
     filename: "avatar.jpg",
     content_type: "image/jpeg",
     metadata: {"identified"=>true, "analyzed"=>true},
     byte_size: 92925,
     checksum: "05OgljCf9r8a47QxgDIoRQ==",
     created_at: Fri, 18 May 2018 11:52:42 UTC +00:00>s
    
      ### ActiveStorage::Attachment.first
     id: 1,
     name: "file",
     record_type: "User",
     record_id: 3,
     blob_id: 1,
     created_at: Fri, 18 May 2018 11:52:42 UTC +00:00>
    

    那么需要显示的时候,如何读取文件呢?

    storage中,文件的读取及删除,都是通过key,你会发现在storage下有一堆文件。具体的文件实现可以在ActiveStorage源代码的lib/service目录下查看。大致的思路是通过make_path_for(key),生成与key值相关的文件夹及文件,然后调用IO.copy_stream(io, make_path_for(key)),将文件的信息写入这些文件中,读取时,只需要读storage下的文件即可。

  • 数据迁移

    还是以User的avatar,单文件上传为例,场景如下:

    先前User的avatar使用传统的uploader来实现上传,现在更换成Storage,这样就会造成一个问题,那就是users表中,有部分的记录,是active_storage_blobs 和active_storage_attachments这两张表中没有的,所以需要将users中的这部分数据迁移过来。

    具体的实现思路是这样的:

    遍历User,对于每一个user,先检查active_storage_attachments中是否存在record_id = user.id && record_type: “User”的纪录,如果存在,则pass,如果不存在,则将该条user纪录添加到表active_storage_blobs 和active_storage_attachments中。

    具体的添加方式有两种:

    一种是直接对数据库进行操作,得到每一个属性的值,然后create即可。但是这里有一个问题,就是active_storage_blobs中的多个字段不那么容易获取,key, checksum,content_type, byte_size 。 我们知道之前上传图片的路径,进而可以读取到图片,通过图片来获取到byte_size,至于content_type则可以通过Mime::Type中的lookup_by_extension来获取,而对于key, checksum, 一番搜索,查看ActiveStorage的源代码,在activestorage/app/models/active_storage/blob.rb中可以知道key值和checksum是如何计算的:

    ##  key
    key = ActiveStorage::Blob.generate_unique_secure_token
    ## checksum
    def compute_checksum_in_chunks(io)
          Digest::MD5.new.tap do |checksum|
            while chunk = io.read(5.megabytes)
              checksum << chunk
            end
            io.rewind
          end.base64digest
        end  
    

    这样大费周章得到这些属性的值后,可以将纪录放入active_storage_blobs中了,随后在active_storage_attachments中也写入对应的纪录就好了。

    如你所见,这样的方法繁琐,而且还有一个致命的缺点,那就是没有了文件转换的步骤。通过直接操作数据库,记录确实补全了,但是你会发现,对于先前上传的文件,在storage下,你是看不到它们对应的文件的,也就是说,用url(avatar)来显示头像时,图片无法显示,读不出来,这就得再增一个步骤,把文件再进行一次处理,处理方式可以在activestorage/lib/active_storage/service/下找到【根据service的不同,有对应的文件,每个文件中都相应的处理方法】。

    第二种方式,则是通过调用接口,也就是ActiveStorage::Blob的类方法,create_after_upload!来实现。这一步不仅建立了新的纪录,而且也在storage下生成了对应的文件用于读取,此外,文件部分,使用Rack::Test::UploadedFile的方法生成临时文件来进行操作。生成新的blob后,再通过ActiveStorageAttachment来创建对应的attachment即可。相比第一种【也就是我之前的思路】,这一种【老大的思路】才是迁移打开的正确方式。

    具体的代码这里就不放了,大致思路如上。

参考

rails source code

Active Storage Overview

Rack::Test::UploadedFile

MIME type