Active Storage 已经发布很久了,都在跃跃欲试吧?我们是否能在我们的系统里面提代替掉 Carrierwave?
最近在一个实际的产品场景里面用上了 Active Storage,实际感受不太好,吃了一鼻子灰。
原因是因为 Active Storage 的设计考虑较多,感觉是针对 签名 URL 的私密存储 (Private ACL) 的场景设计的,自带的功能以及 Rails Guides 里面的指导 带出来的结果最终生成的还是偏向于签名 URL 生成。
然而我们往往在 Rails 项目里面用的最多的反而是公开存储的场景,例如:用户头像、公司 Logo、新闻附件、图片等,这些不需要那么复杂的鉴权签名机制,同时新闻图片这类场景我们需要一个固定 URL 地址,以便于插入到 Rich HTML 里面去,否则像 Acitve Storage 默认的那样,URL 有有效期,这可没法玩。
我分析发现默认情况下,Active Storage 生成的 URL 地址包含两部分(Blob key 的签名段,缩略图规则的签名段),这两段签名包含有效期,同时基于 secret_key_base
来签名。
也就是说假如有一天,我们的 secret_key_base
改变了(这个是很有可能发生的),那么就算图片 URL 没有效期,插入到 HTML 里面的图片地址依然会失效。然后这个问题可能还会在你的系统跑了一段时间以后出现。
于是我今天发了几条 Tweet 说了一下这个事情:
https://twitter.com/huacnlee/status/1057844908654977024
关于上面的问题,Rails Issues 里面也有人反馈,但看起来也没给出正确的解决方案(我怀疑他们可能都不用 :disk
存储)。
https://github.com/rails/rails/issues/31419
于是乎,翻了一些资料,以及阅读 Active Storage 的实现,我找到了一个稳妥的解决方案。
这里以 :disk
的存储场景距离,使用 Aliyun 的可以跳过,直接用我弄好的 activestorage-aliyun 即可,用 service_url
生成的 URL 不会有上面说的问题。
解决方式大致是新增一个 uploads_controller
用来代替 Active Storage 自带的 Controller:
class UploadsController < ApplicationController
before_action :require_disk_service!
before_action :set_blob
# GET /upload/:id
# GET /upload/:id?s=large
def show
if params[:s]
# generate thumb
variation_key = Blob.variation(params[:s])
send_file_by_disk_key @blob.representation(variation_key).processed.key, content_type: @blob.content_type
else
# return raw file
send_file_by_disk_key @blob.key, content_type: @blob.content_type, disposition: params[:disposition], filename: @blob.filename
end
end
private
def send_file_by_disk_key(key, content_type:, disposition: :inline, filename: nil)
opts = { type: content_type, disposition: disposition, filename: filename }
send_file Blob.path_for(key), opts
end
def set_blob
@blob = ActiveStorage::Blob.find_by(key: params[:id])
head :not_found if @blob.blank?
end
def require_disk_service!
head :not_found unless Blob.disk_service?
end
end
config/routes.rb 新增一行
resources :uploads
需要一个 app/model/blob.rb,用来自定义缩略图名称,以及对于的 ImageMagick 转换规则,比如我这个:
class Blob
class << self
IMAGE_SIZES = { tiny: 32, small: 64, medium: 96, xlarge: 2400 }
def disk_service?
ActiveStorage::Blob.service.send(:service_name) == "Disk"
end
def variation(style)
ActiveStorage::Variation.new(combine_options: combine_options(style))
end
def combine_options(style)
style = style.to_sym
size = IMAGE_SIZES[style] || IMAGE_SIZES[:small]
if style == :xlarge
{ resize: "#{size}>" }
else
{ thumbnail: "#{size}x#{size}^", gravity: "center", extent: "#{size}x#{size}" }
end
end
def path_for(key)
ActiveStorage::Blob.service.send(:path_for, key)
end
end
end
最后,需要用到图片、附件固定 URL 的场景,改用 upload_path
代替:
<%= image_tag upload_path(@user.avatar.blob.key, s: :small) %>
最后可以得到这样的结果:
我整理了一个例子项目,有需要的可以拉下来,跑起来看看: