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即可。相比第一种【也就是我之前的思路】,这一种【老大的思路】才是迁移打开的正确方式。
具体的代码这里就不放了,大致思路如上。