1 什么是 Active Storage?
Active Storage 促进将文件上传到云存储服务(如 Amazon S3、Google Cloud Storage 或 Microsoft Azure Storage)并将这些文件附加到 Active Record 对象。它附带一个用于开发和测试的本地磁盘服务,并支持将文件镜像到下级服务以进行备份和迁移。
使用 Active Storage,应用程序可以转换图像上传或生成非图像上传(如 PDF 和视频)的图像表示形式,并从任意文件中提取元数据。
1.1 要求
Active Storage 的各种功能依赖于 Rails 不会安装的第三方软件,必须单独安装
- libvips v8.6+ 或 ImageMagick 用于图像分析和转换
- ffmpeg v3.4+ 用于视频预览和 ffprobe 用于视频/音频分析
- poppler 或 muPDF 用于 PDF 预览
图像分析和转换还需要 image_processing
gem。在您的 Gemfile
中取消注释它,或者在必要时添加它
gem "image_processing", ">= 1.2"
与 libvips 相比,ImageMagick 更广为人知,而且可用性更高。但是,libvips 可以 快 10 倍,内存消耗仅为 1/10。对于 JPEG 文件,可以通过将 libjpeg-dev
替换为 libjpeg-turbo-dev
来进一步提高速度,后者 快 2-7 倍。
在安装和使用第三方软件之前,请确保您了解这样做的许可证影响。尤其是 MuPDF,其许可证为 AGPL,某些用途需要商业许可证。
2 设置
$ bin/rails active_storage:install
$ bin/rails db:migrate
这将设置配置,并创建 Active Storage 使用的三个表:active_storage_blobs
、active_storage_attachments
和 active_storage_variant_records
。
表 | 目的 |
---|---|
active_storage_blobs |
存储有关上传文件的数据,例如文件名和内容类型。 |
active_storage_attachments |
一个多态联接表,将您的模型连接到 blob。如果您的模型类名更改,则需要在此表上运行迁移以将基础 record_type 更新为您的模型的新类名。 |
active_storage_variant_records |
如果启用了变体跟踪,则为每个已生成的变体存储记录。 |
如果您使用 UUID 而不是整数作为模型上的主键,则应在配置文件中设置 Rails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid }
。
在 config/storage.yml
中声明 Active Storage 服务。对于您的应用程序使用的每个服务,请提供一个名称和必要的配置。以下示例声明了三个名为 local
、test
和 amazon
的服务
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
bucket: your_own_bucket-<%= Rails.env %>
region: "" # e.g. 'us-east-1'
通过设置 Rails.application.config.active_storage.service
告诉 Active Storage 使用哪个服务。由于每个环境可能使用不同的服务,建议在每个环境的基础上执行此操作。要在开发环境中使用上一个示例中的磁盘服务,您需要在 config/environments/development.rb
中添加以下内容
# Store files locally.
config.active_storage.service = :local
要在生产环境中使用 S3 服务,您需要在 config/environments/production.rb
中添加以下内容
# Store files on Amazon S3.
config.active_storage.service = :amazon
要在测试时使用测试服务,您需要在 config/environments/test.rb
中添加以下内容
# Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
特定于环境的配置文件将优先考虑:例如,在生产环境中,config/storage/production.yml
文件(如果存在)将优先于 config/storage.yml
文件。
建议在存储桶名称中使用 Rails.env
,以进一步降低意外销毁生产数据的风险。
amazon:
service: S3
# ...
bucket: your_own_bucket-<%= Rails.env %>
google:
service: GCS
# ...
bucket: your_own_bucket-<%= Rails.env %>
azure:
service: AzureStorage
# ...
container: your_container_name-<%= Rails.env %>
继续阅读以获取有关内置服务适配器(例如 Disk
和 S3
)及其所需的配置的更多信息。
2.1 磁盘服务
在 config/storage.yml
中声明一个磁盘服务
local:
service: Disk
root: <%= Rails.root.join("storage") %>
2.2 S3 服务(Amazon S3 和与 S3 兼容的 API)
要连接到 Amazon S3,请在 config/storage.yml
中声明一个 S3 服务
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # e.g. 'us-east-1'
bucket: your_own_bucket-<%= Rails.env %>
可以选择提供客户端和上传选项
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # e.g. 'us-east-1'
bucket: your_own_bucket-<%= Rails.env %>
http_open_timeout: 0
http_read_timeout: 0
retry_limit: 0
upload:
server_side_encryption: "" # 'aws:kms' or 'AES256'
cache_control: "private, max-age=<%= 1.day.to_i %>"
为您的应用程序设置合理的客户端 HTTP 超时和重试限制。在某些故障情况下,默认的 AWS 客户端配置可能会导致连接保持长达数分钟,并导致请求排队。
将 aws-sdk-s3
gem 添加到您的 Gemfile
gem "aws-sdk-s3", require: false
Active Storage 的核心功能需要以下权限:s3:ListBucket
、s3:PutObject
、s3:GetObject
和 s3:DeleteObject
。 公共访问 还需要 s3:PutObjectAcl
。如果您配置了其他上传选项(例如设置 ACL),则可能需要其他权限。
如果您要使用环境变量、标准 SDK 配置文件、配置文件、IAM 实例配置文件或任务角色,则可以在上面的示例中省略 access_key_id
、secret_access_key
和 region
键。S3 服务支持 AWS SDK 文档 中描述的所有身份验证选项。
要连接到与 S3 兼容的对象存储 API(例如 DigitalOcean Spaces),请提供 endpoint
digitalocean:
service: S3
endpoint: https://nyc3.digitaloceanspaces.com
access_key_id: <%= Rails.application.credentials.dig(:digitalocean, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:digitalocean, :secret_access_key) %>
# ...and other options
还有许多其他选项可用。您可以在 AWS S3 客户端 文档中查看它们。
2.3 Microsoft Azure 存储服务
在 config/storage.yml
中声明一个 Azure 存储服务
# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
azure:
service: AzureStorage
storage_account_name: your_account_name
storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
container: your_container_name-<%= Rails.env %>
将 azure-storage-blob
gem 添加到您的 Gemfile
gem "azure-storage-blob", "~> 2.0", require: false
2.4 Google Cloud 存储服务
在 config/storage.yml
中声明一个 Google Cloud 存储服务
google:
service: GCS
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
project: ""
bucket: your_own_bucket-<%= Rails.env %>
可以选择提供凭据的哈希而不是密钥文件路径
# Use bin/rails credentials:edit to set the GCS secrets (as gcs:private_key_id|private_key)
google:
service: GCS
credentials:
type: "service_account"
project_id: ""
private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
client_email: ""
client_id: ""
auth_uri: "https://accounts.google.com/o/oauth2/auth"
token_uri: "https://accounts.google.com/o/oauth2/token"
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url: ""
project: ""
bucket: your_own_bucket-<%= Rails.env %>
可以选择提供一个 Cache-Control 元数据,以便在上传的资产上设置
google:
service: GCS
...
cache_control: "public, max-age=3600"
可以选择使用 IAM 而不是 credentials
来签署 URL。如果您使用 Workload Identity 来验证您的 GKE 应用程序,这将非常有用,有关更多信息,请参见 这篇 Google Cloud 博客文章。
google:
service: GCS
...
iam: true
可选地在签署 URL 时使用特定 GSA。当使用 IAM 时,元数据服务器 将被联系以获取 GSA 邮箱,但此元数据服务器并不总是存在(例如本地测试),您可能希望使用非默认 GSA。
google:
service: GCS
...
iam: true
gsa_email: "[email protected]"
将 google-cloud-storage
gem 添加到您的 Gemfile
中。
gem "google-cloud-storage", "~> 1.11", require: false
2.5 镜像服务
您可以通过定义镜像服务来使多个服务保持同步。镜像服务在两个或多个下属服务之间复制上传和删除操作。
镜像服务旨在在生产环境中服务迁移期间临时使用。您可以开始镜像到新服务,将旧服务中的预先存在的文件复制到新服务,然后全面切换到新服务。
镜像不是原子操作。上传可能会在主服务上成功,但在任何下属服务上失败。在全面切换到新服务之前,请验证所有文件是否已复制。
按照上述描述定义您要镜像的每个服务。在定义镜像服务时,通过名称引用它们。
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
s3_west_coast:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # e.g. 'us-west-1'
bucket: your_own_bucket-<%= Rails.env %>
s3_east_coast:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: "" # e.g. 'us-east-1'
bucket: your_own_bucket-<%= Rails.env %>
production:
service: Mirror
primary: s3_east_coast
mirrors:
- s3_west_coast
尽管所有辅助服务都接收上传,但下载始终由主服务处理。
镜像服务与直接上传兼容。新文件直接上传到主服务。当直接上传的文件附加到记录时,会排队一个后台作业以将其复制到辅助服务。
2.6 公开访问
默认情况下,Active Storage 假设对服务的私有访问。这意味着为 Blob 生成已签名的、一次性使用的 URL。如果您希望使 Blob 公开访问,请在您的应用的 config/storage.yml
中指定 public: true
。
gcs: &gcs
service: GCS
project: ""
private_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/private_key.json") %>
bucket: your_own_bucket-<%= Rails.env %>
public_gcs:
<<: *gcs
credentials: <%= Rails.root.join("path/to/public_key.json") %>
bucket: your_own_bucket-<%= Rails.env %>
public: true
确保您的存储桶已针对公开访问进行了正确配置。查看有关如何为 Amazon S3、Google Cloud Storage 和 Microsoft Azure 存储服务启用公开读取权限的文档。Amazon S3 此外还要求您具有 s3:PutObjectAcl
权限。
在将现有应用程序转换为使用 public: true
时,请确保在切换之前更新存储桶中的每个单独文件以使其公开可读。
3 将文件附加到记录
3.1 has_one_attached
has_one_attached
宏在记录和文件之间建立一对一映射。每个记录可以附加一个文件。
例如,假设您的应用程序有一个 User
模型。如果您希望每个用户都拥有一个头像,请将 User
模型定义如下:
class User < ApplicationRecord
has_one_attached :avatar
end
或者,如果您使用的是 Rails 6.0+,您可以运行类似于以下的模型生成器命令:
$ bin/rails generate model User avatar:attachment
您可以创建一个带有头像的用户:
<%= form.file_field :avatar %>
class SignupController < ApplicationController
def create
user = User.create!(user_params)
session[:user_id] = user.id
redirect_to root_path
end
private
def user_params
params.expect(user: [:email_address, :password, :avatar])
end
end
调用 avatar.attach
将头像附加到现有用户:
user.avatar.attach(params[:avatar])
调用 avatar.attached?
以确定特定用户是否拥有头像:
user.avatar.attached?
在某些情况下,您可能希望为特定附件覆盖默认服务。您可以使用 service
选项以及您的服务名称来为每个附件配置特定服务:
class User < ApplicationRecord
has_one_attached :avatar, service: :google
end
您可以通过在生成的 attachable 对象上调用 variant
方法来为每个附件配置特定变体:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
调用 avatar.variant(:thumb)
以获取头像的缩略图变体:
<%= image_tag user.avatar.variant(:thumb) %>
您也可以使用特定变体进行预览:
class User < ApplicationRecord
has_one_attached :video do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
<%= image_tag user.video.preview(:thumb) %>
如果您事先知道将访问您的变体,则可以指定 Rails 应该提前生成它们:
class User < ApplicationRecord
has_one_attached :video do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100], preprocessed: true
end
end
在附件附加到记录后,Rails 将排队一个作业来生成变体。
由于 Active Storage 依赖于多态关联,而 多态关联 依赖于在数据库中存储类名,因此这些数据必须与 Ruby 代码使用的类名保持同步。当重命名使用 has_one_attached
的类时,请确保还更新相应行的 active_storage_attachments.record_type
多态类型列中的类名。
3.2 has_many_attached
has_many_attached
宏在记录和文件之间建立一对多关系。每个记录可以附加多个文件。
例如,假设您的应用程序有一个 Message
模型。如果您希望每条消息都拥有多个图像,请将 Message
模型定义如下:
class Message < ApplicationRecord
has_many_attached :images
end
或者,如果您使用的是 Rails 6.0+,您可以运行类似于以下的模型生成器命令:
$ bin/rails generate model Message images:attachments
您可以创建一个带有图像的消息:
class MessagesController < ApplicationController
def create
message = Message.create!(message_params)
redirect_to message
end
private
def message_params
params.expect(message: [ :title, :content, images: [] ])
end
end
调用 images.attach
将新图像添加到现有消息:
@message.images.attach(params[:images])
调用 images.attached?
以确定特定消息是否包含任何图像:
@message.images.attached?
覆盖默认服务的方式与 has_one_attached
相同,使用 service
选项:
class Message < ApplicationRecord
has_many_attached :images, service: :s3
end
配置特定变体的方式与 has_one_attached
相同,在生成的 attachable 对象上调用 variant
方法:
class Message < ApplicationRecord
has_many_attached :images do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
由于 Active Storage 依赖于多态关联,而 多态关联 依赖于在数据库中存储类名,因此这些数据必须与 Ruby 代码使用的类名保持同步。当重命名使用 has_many_attached
的类时,请确保还更新相应行的 active_storage_attachments.record_type
多态类型列中的类名。
3.3 附加文件/IO 对象
有时您需要附加一个未通过 HTTP 请求到达的文件。例如,您可能希望附加一个您在磁盘上生成或从用户提交的 URL 下载的文件。您可能还希望在模型测试中附加一个固定装置文件。为此,请提供一个至少包含一个打开的 IO 对象和一个文件名的 Hash:
@message.images.attach(io: File.open("/path/to/file"), filename: "file.pdf")
如果可能,也提供一个内容类型。Active Storage 尝试从其数据中确定文件的类型。如果无法做到这一点,它将回退到您提供的内容类型。
@message.images.attach(io: File.open("/path/to/file"), filename: "file.pdf", content_type: "application/pdf")
您可以通过传递 identify: false
以及 content_type
来绕过从数据中推断内容类型。
@message.images.attach(
io: File.open("/path/to/file"),
filename: "file.pdf",
content_type: "application/pdf",
identify: false
)
如果您不提供内容类型,而 Active Storage 无法自动确定文件的类型,它将默认为 application/octet-stream。
还有一个额外的参数 key
,可用于指定 S3 存储桶中的文件夹/子文件夹。AWS S3 否则将使用随机密钥来命名您的文件。如果您希望更好地组织 S3 存储桶文件,此方法很有用。
@message.images.attach(
io: File.open("/path/to/file"),
filename: "file.pdf",
content_type: "application/pdf",
key: "#{Rails.env}/blog_content/intuitive_filename.pdf",
identify: false
)
这样,当您从开发环境测试时,文件将保存到 [S3_BUCKET]/development/blog_content/
文件夹中。请注意,如果您使用 key
参数,则必须确保密钥对于上传是唯一的。建议将文件名附加一个唯一的随机密钥,例如:
def s3_file_key
"#{Rails.env}/blog_content/intuitive_filename-#{SecureRandom.uuid}.pdf"
end
@message.images.attach(
io: File.open("/path/to/file"),
filename: "file.pdf",
content_type: "application/pdf",
key: s3_file_key,
identify: false
)
3.4 替换与添加附件
在 Rails 中,默认情况下,将文件附加到 has_many_attached
关联将替换任何现有的附件。
要保留现有附件,可以使用带有每个附加文件的 signed_id
的隐藏表单字段:
<% @message.images.each do |image| %>
<%= form.hidden_field :images, multiple: true, value: image.signed_id %>
<% end %>
<%= form.file_field :images, multiple: true %>
这具有使您能够选择性地删除现有附件的优点,例如使用 JavaScript 删除各个隐藏字段。
3.5 表单验证
附件不会在关联记录上成功 save
之前发送到存储服务。这意味着,如果表单提交失败验证,任何新的附件都将丢失,并且必须重新上传。由于 直接上传 在提交表单之前就被存储了,因此它们可以用于在验证失败时保留上传内容。
<%= form.hidden_field :avatar, value: @user.avatar.signed_id if @user.avatar.attached? %>
<%= form.file_field :avatar, direct_upload: true %>
4 删除文件
要从模型中删除附件,请在附件上调用 purge
。如果您的应用程序设置为使用 Active Job,则可以通过调用 purge_later
在后台完成删除操作。清除将删除 Blob 和存储服务中的文件。
# Synchronously destroy the avatar and actual resource files.
user.avatar.purge
# Destroy the associated models and actual resource files async, via Active Job.
user.avatar.purge_later
5 提供文件
Active Storage 支持两种提供文件的方式:重定向和代理。
默认情况下,所有 Active Storage 控制器都是公开可访问的。生成的 URL 很难猜测,但设计为永久性。如果您的文件需要更高的保护级别,请考虑实施 已认证的控制器。
5.1 重定向模式
要为 Blob 生成永久性 URL,您可以将 Blob 传递给 url_for
视图帮助程序。这将生成一个包含 Blob 的 signed_id
的 URL,该 URL 将路由到 Blob 的 RedirectController
。
url_for(user.avatar)
# => https://www.example.com/rails/active_storage/blobs/redirect/:signed_id/my-avatar.png
RedirectController
将重定向到实际的服务端点。这种间接方式使服务 URL 与实际 URL 脱钩,并允许例如在不同服务中镜像附件以实现高可用性。重定向具有 5 分钟的 HTTP 过期时间。
要创建下载链接,请使用 rails_blob_{path|url}
帮助程序。使用此帮助程序可以设置处置。
rails_blob_path(user.avatar, disposition: "attachment")
为了防止 XSS 攻击,Active Storage 迫使 Content-Disposition 标头为某些类型文件的 "attachment"。要更改此行为,请参阅 配置 Rails 应用程序 中提供的配置选项。
如果您需要从控制器/视图上下文之外(后台作业、Cron 作业等)创建链接,您可以像这样访问 rails_blob_path
:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
5.2 代理模式
可选地,可以代理文件。这意味着您的应用程序服务器将在响应请求时从存储服务下载文件数据。这对于从 CDN 提供文件很有用。
您可以将 Active Storage 配置为默认使用代理:
# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy
或者,如果您希望明确代理特定附件,可以使用 rails_storage_proxy_path
和 rails_storage_proxy_url
形式的 URL 帮助程序。
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
5.2.1 将 CDN 放在 Active Storage 前面
此外,为了将 CDN 用于 Active Storage 附件,您将需要使用代理模式生成 URL,以便它们由您的应用程序提供服务,并且 CDN 将在没有任何额外配置的情况下缓存附件。这在开箱即用时有效,因为默认的 Active Storage 代理控制器设置了一个 HTTP 标头,指示 CDN 缓存响应。
您还应该确保生成的 URL 使用 CDN 主机而不是您的应用程序主机。有多种方法可以实现这一点,但一般而言,这涉及调整您的 config/routes.rb
文件,以便您可以为附件及其变体生成正确的 URL。例如,您可以添加以下内容:
# config/routes.rb
direct :cdn_image do |model, options|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
if model.respond_to?(:signed_id)
route_for(
:rails_service_blob_proxy,
model.signed_id(expires_in: expires_in),
model.filename,
options.merge(host: ENV["CDN_HOST"])
)
else
signed_blob_id = model.blob.signed_id(expires_in: expires_in)
variation_key = model.variation.key
filename = model.blob.filename
route_for(
:rails_blob_representation_proxy,
signed_blob_id,
variation_key,
filename,
options.merge(host: ENV["CDN_HOST"])
)
end
end
然后像这样生成路由:
<%= cdn_image_url(user.avatar.variant(resize_to_limit: [128, 128])) %>
5.3 经过身份验证的控制器
默认情况下,所有 Active Storage 控制器都是公开可访问的。生成的 URL 使用普通的 signed_id
,这使得它们难以猜测但永久存在。任何知道 blob URL 的人都可以访问它,即使您 ApplicationController
中的 before_action
否则需要登录。如果您的文件需要更高级别的保护,您可以基于 ActiveStorage::Blobs::RedirectController
、ActiveStorage::Blobs::ProxyController
、ActiveStorage::Representations::RedirectController
和 ActiveStorage::Representations::ProxyController
实现您自己的经过身份验证的控制器。
要仅允许帐户访问自己的徽标,您可以执行以下操作
# config/routes.rb
resource :account do
resource :logo
end
# app/controllers/logos_controller.rb
class LogosController < ApplicationController
# Through ApplicationController:
# include Authenticate, SetCurrentAccount
def show
redirect_to Current.account.logo.url
end
end
<%= image_tag account_logo_path %>
然后,您应该使用以下命令禁用 Active Storage 默认路由
config.active_storage.draw_routes = false
以防止使用公开可访问的 URL 访问文件。
6 下载文件
有时,您需要在上传文件后处理 blob,例如将其转换为不同的格式。使用附件的 download
方法将 blob 的二进制数据读入内存
binary = user.avatar.download
您可能希望将 blob 下载到磁盘上的文件,以便外部程序(例如病毒扫描程序或媒体转码器)可以对其进行操作。使用附件的 open
方法将 blob 下载到磁盘上的临时文件
message.video.open do |file|
system "/path/to/virus/scanner", file.path
# ...
end
重要的是要注意,该文件在 after_create
回调中尚不可用,而仅在 after_create_commit
中可用。
7 分析文件
Active Storage 在文件上传后通过在 Active Job 中排队作业来分析文件。分析的文件将在元数据哈希中存储附加信息,包括 analyzed: true
。您可以通过调用 analyzed?
来检查 blob 是否已分析。
图像分析提供 width
和 height
属性。视频分析提供这些属性,以及 duration
、angle
、display_aspect_ratio
以及 video
和 audio
布尔值来指示这些通道的存在。音频分析提供 duration
和 bit_rate
属性。
8 显示图像、视频和 PDF
Active Storage 支持表示各种文件。您可以调用附件的 representation
来显示图像变体,或显示视频或 PDF 的预览。在调用 representation
之前,请检查附件是否可以通过调用 representable?
来表示。某些文件格式不能由 Active Storage 开箱即用地预览(例如 Word 文档);如果 representable?
返回 false,您可能希望改为 链接到 该文件。
<ul>
<% @message.files.each do |file| %>
<li>
<% if file.representable? %>
<%= image_tag file.representation(resize_to_limit: [100, 100]) %>
<% else %>
<%= link_to rails_blob_path(file, disposition: "attachment") do %>
<%= image_tag "placeholder.png", alt: "Download file" %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
在内部,representation
为图像调用 variant
,为可预览的文件调用 preview
。您也可以直接调用这些方法。
8.1 延迟加载与立即加载
默认情况下,Active Storage 将延迟处理表示。这段代码
image_tag file.representation(resize_to_limit: [100, 100])
将生成一个 <img>
标签,其 src
指向 ActiveStorage::Representations::RedirectController
。浏览器将向该控制器发出请求,该控制器将执行以下操作
- 处理文件,如果必要,上传处理后的文件。
- 返回一个
302
重定向到文件,可以是- 远程服务(例如 S3)。
- 或
ActiveStorage::Blobs::ProxyController
,如果启用了 代理模式,它将返回文件内容。
延迟加载文件允许诸如 单次使用 URL 之类的功能在不减慢初始页面加载速度的情况下工作。
这对大多数情况都适用。
如果您想立即为图像生成 URL,可以调用 .processed.url
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
Active Storage 变体跟踪器通过在数据库中存储记录(如果之前已处理请求的表示)来提高此性能。因此,上面的代码只会对远程服务(例如 S3)进行一次 API 调用,并且一旦存储了变体,就会使用它。变体跟踪器会自动运行,但可以通过 config.active_storage.track_variants
禁用。
如果您在一个页面上呈现大量图像,上面的示例可能会导致 N+1 查询来加载所有变体记录。为了避免这些 N+1 查询,请使用 ActiveStorage::Attachment
上的命名范围。
message.images.with_all_variant_records.each do |file|
image_tag file.representation(resize_to_limit: [100, 100]).processed.url
end
8.2 转换图像
转换图像允许您根据您的选择显示图像的尺寸。要创建图像的变体,请在附件上调用 variant
。您可以将变体处理器支持的任何转换传递给该方法。当浏览器访问变体 URL 时,Active Storage 将延迟地将原始 blob 转换为指定的格式并重定向到其新的服务位置。
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
如果请求了变体,Active Storage 将根据图像的格式自动应用转换
内容类型是可变的(由
config.active_storage.variable_content_types
决定)且不被视为 Web 图像(由config.active_storage.web_image_content_types
决定),将被转换为 PNG。如果未指定
quality
,将使用变体处理器的格式的默认质量。
Active Storage 可以使用 Vips 或 MiniMagick 作为变体处理器。默认值取决于您的 config.load_defaults
目标版本,可以通过设置 config.active_storage.variant_processor
来更改处理器。
可用的参数由 image_processing
gem 定义,并且取决于您正在使用的变体处理器,但两者都支持以下参数
参数 | 示例 | 描述 |
---|---|---|
resize_to_limit |
resize_to_limit: [100, 100] |
将图像缩小以适合指定的尺寸,同时保持原始纵横比。如果图像大于指定的尺寸,则只会调整图像大小。 |
resize_to_fit |
resize_to_fit: [100, 100] |
将图像调整大小以适合指定的尺寸,同时保持原始纵横比。如果图像大于指定的尺寸,则会缩小图像,如果小于,则会放大。 |
resize_to_fill |
resize_to_fill: [100, 100] |
将图像调整大小以填充指定的尺寸,同时保持原始纵横比。如果需要,将在较大尺寸上裁剪图像。 |
resize_and_pad |
resize_and_pad: [100, 100] |
将图像调整大小以适合指定的尺寸,同时保持原始纵横比。如果需要,将使用透明颜色填充剩余区域(如果源图像具有 alpha 通道,否则使用黑色)。 |
crop |
crop: [20, 50, 300, 300] |
从图像中提取一个区域。前两个参数是要提取区域的左边缘和上边缘,后两个参数是要提取区域的宽度和高度。 |
rotate |
rotate: 90 |
按指定的角度旋转图像。 |
image_processing
在其自己的文档中提供了所有参数,适用于 Vips 和 MiniMagick 处理器。
某些参数(包括上面列出的参数)接受额外的处理器特定选项,这些选项可以作为 key: value
对以哈希的形式传递
<!-- Vips supports configuring `crop` for many of its transformations -->
<%= image_tag user.avatar.variant(resize_to_fill: [100, 100, { crop: :centre }]) %>
如果在 MiniMagick 和 Vips 之间迁移现有应用程序,则需要更新处理器特定选项
<!-- MiniMagick -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80) %>
<!-- Vips -->
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100], format: :jpeg, saver: { subsample_mode: "on", strip: true, interlace: true, quality: 80 }) %>
8.3 预览文件
一些非图像文件可以预览:即它们可以作为图像呈现。例如,可以通过提取视频文件的首帧来预览视频文件。开箱即用,Active Storage 支持预览视频和 PDF 文档。要创建指向延迟生成的预览的链接,请使用附件的 preview
方法
<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>
要添加对另一种格式的支持,请添加您自己的预览器。有关更多信息,请参阅 ActiveStorage::Preview
文档。
9 直接上传
Active Storage 及其包含的 JavaScript 库支持直接从客户端上传到云。
9.1 用法
在您的应用程序的 JavaScript 包中包含
activestorage.js
。使用资产管道
//= require activestorage
使用 npm 包
import * as ActiveStorage from "@rails/activestorage" ActiveStorage.start()
将
direct_upload: true
添加到您的 文件字段<%= form.file_field :attachments, multiple: true, direct_upload: true %>
或者,如果您没有使用
FormBuilder
,请直接添加数据属性<input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
在第三方存储服务上配置 CORS 以允许直接上传请求。
就这样!上传将在提交表单后开始。
9.2 跨域资源共享 (CORS) 配置
要使对第三方服务的直接上传起作用,您需要配置该服务以允许来自您的应用程序的跨域请求。请参阅您服务的 CORS 文档
注意允许
- 您的应用程序访问的所有来源
PUT
请求方法- 以下标题
Content-Type
Content-MD5
Content-Disposition
(Azure 存储除外)x-ms-blob-content-disposition
(仅限 Azure 存储)x-ms-blob-type
(仅限 Azure 存储)Cache-Control
(对于 GCS,仅当设置了cache_control
时)
磁盘服务不需要 CORS 配置,因为它与您的应用程序的来源共享。
9.2.1 示例:S3 CORS 配置
[
{
"AllowedHeaders": [
"Content-Type",
"Content-MD5",
"Content-Disposition"
],
"AllowedMethods": [
"PUT"
],
"AllowedOrigins": [
"https://www.example.com"
],
"MaxAgeSeconds": 3600
}
]
9.2.2 示例:Google Cloud Storage CORS 配置
[
{
"origin": ["https://www.example.com"],
"method": ["PUT"],
"responseHeader": ["Content-Type", "Content-MD5", "Content-Disposition"],
"maxAgeSeconds": 3600
}
]
9.2.3 示例:Azure 存储 CORS 配置
<Cors>
<CorsRule>
<AllowedOrigins>https://www.example.com</AllowedOrigins>
<AllowedMethods>PUT</AllowedMethods>
<AllowedHeaders>Content-Type, Content-MD5, x-ms-blob-content-disposition, x-ms-blob-type</AllowedHeaders>
<MaxAgeInSeconds>3600</MaxAgeInSeconds>
</CorsRule>
</Cors>
9.3 直接上传 JavaScript 事件
事件名称 | 事件目标 | 事件数据 (event.detail ) |
描述 |
---|---|---|---|
direct-uploads:start |
<form> |
无 | 提交了包含用于直接上传字段的文件的表单。 |
direct-upload:initialize |
<input> |
{id, file} |
表单提交后,每个文件都会派发。 |
direct-upload:start |
<input> |
{id, file} |
直接上传开始。 |
direct-upload:before-blob-request |
<input> |
{id, file, xhr} |
在向您的应用程序发出直接上传元数据的请求之前。 |
direct-upload:before-storage-request |
<input> |
{id, file, xhr} |
在发出存储文件的请求之前。 |
direct-upload:progress |
<input> |
{id, file, progress} |
当存储文件的请求进行时。 |
direct-upload:error |
<input> |
{id, file, error} |
发生错误。除非取消此事件,否则将显示alert 。 |
direct-upload:end |
<input> |
{id, file} |
直接上传已结束。 |
direct-uploads:end |
<form> |
无 | 所有直接上传都已结束。 |
9.4 示例
您可以使用这些事件来显示上传的进度。
在表单中显示已上传的文件
// direct_uploads.js
addEventListener("direct-upload:initialize", event => {
const { target, detail } = event
const { id, file } = detail
target.insertAdjacentHTML("beforebegin", `
<div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
<div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename"></span>
</div>
`)
target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent = file.name
})
addEventListener("direct-upload:start", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.remove("direct-upload--pending")
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail
const progressElement = document.getElementById(`direct-upload-progress-${id}`)
progressElement.style.width = `${progress}%`
})
addEventListener("direct-upload:error", event => {
event.preventDefault()
const { id, error } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--error")
element.setAttribute("title", error)
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--complete")
})
添加样式
/* direct_uploads.css */
.direct-upload {
display: inline-block;
position: relative;
padding: 2px 4px;
margin: 0 3px 3px 0;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 3px;
font-size: 11px;
line-height: 13px;
}
.direct-upload--pending {
opacity: 0.6;
}
.direct-upload__progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
opacity: 0.2;
background: #0076ff;
transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
transform: translate3d(0, 0, 0);
}
.direct-upload--complete .direct-upload__progress {
opacity: 0.4;
}
.direct-upload--error {
border-color: red;
}
input[type=file][data-direct-upload-url][disabled] {
display: none;
}
9.5 自定义拖放解决方案
您可以为此目的使用DirectUpload
类。从您选择的库中接收文件后,实例化一个 DirectUpload 并调用其 create 方法。Create 接受一个回调,在上传完成后调用该回调。
import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
// Bind to file drop - use the ondrop on a parent element or use a
// library like Dropzone
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// Bind to normal file selection
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// you might clear the selected files from the input
input.value = null
})
const uploadFile = (file) => {
// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// Add an appropriately-named hidden input to the form with a
// value of blob.signed_id so that the blob ids will be
// transmitted in the normal upload flow
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
9.6 跟踪文件上传进度
使用DirectUpload
构造函数时,可以包含第三个参数。这将允许DirectUpload
对象在上传过程中调用directUploadWillStoreFileWithXHR
方法。然后,您可以将自己的进度处理程序附加到 XHR 以满足您的需求。
import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url) {
this.upload = new DirectUpload(file, url, this)
}
uploadFile(file) {
this.upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// Add an appropriately-named hidden input to the form
// with a value of blob.signed_id
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// Use event.loaded and event.total to update the progress bar
}
}
9.7 与库或框架集成
从您选择的库中接收文件后,您需要创建一个DirectUpload
实例,并使用其“create”方法来启动上传过程,并根据需要添加任何所需的额外标头。“create”方法还需要提供一个回调函数,该函数将在上传完成后触发。
import { DirectUpload } from "@rails/activestorage"
class Uploader {
constructor(file, url, token) {
const headers = { 'Authentication': `Bearer ${token}` }
// INFO: Sending headers is an optional parameter. If you choose not to send headers,
// authentication will be performed using cookies or session data.
this.upload = new DirectUpload(file, url, this, headers)
}
uploadFile(file) {
this.upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
// Use the with blob.signed_id as a file reference in next request
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// Use event.loaded and event.total to update the progress bar
}
}
要实现自定义身份验证,必须在 Rails 应用程序上创建一个新的控制器,类似于以下内容
class DirectUploadsController < ActiveStorage::DirectUploadsController
skip_forgery_protection
before_action :authenticate!
def authenticate!
@token = request.headers["Authorization"]&.split&.last
head :unauthorized unless valid_token?(@token)
end
end
10 测试
使用file_fixture_upload
在集成或控制器测试中测试上传文件。Rails 像处理任何其他参数一样处理文件。
class SignupController < ActionDispatch::IntegrationTest
test "can sign up" do
post signup_path, params: {
name: "David",
avatar: file_fixture_upload("david.png", "image/png")
}
user = User.order(:created_at).last
assert user.avatar.attached?
end
end
10.1 丢弃测试期间创建的文件
10.1.1 系统测试
系统测试通过回滚事务来清理测试数据。因为destroy
从未在对象上调用,所以附加的文件从未被清理。如果要清除文件,可以在after_teardown
回调中执行此操作。在此处执行此操作可确保在测试期间创建的所有连接都已完成,并且您不会收到 Active Storage 的错误,说明它找不到文件。
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
# ...
end
如果您正在使用并行测试和DiskService
,您应该配置每个进程以使用自己的 Active Storage 文件夹。这样,teardown
回调将只删除相关进程测试中的文件。
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# ...
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
# ...
end
如果您的系统测试验证了带有附件的模型的删除,并且您正在使用 Active Job,请将您的测试环境设置为使用内联队列适配器,以便立即执行清除作业,而不是在将来的未知时间执行。
# Use inline job processing to make things happen immediately
config.active_job.queue_adapter = :inline
10.1.2 集成测试
与系统测试类似,集成测试期间上传的文件不会自动清理。如果要清除文件,可以在teardown
回调中执行此操作。
class ActionDispatch::IntegrationTest
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
end
如果您正在使用并行测试和 Disk 服务,您应该配置每个进程以使用自己的 Active Storage 文件夹。这样,teardown
回调将只删除相关进程测试中的文件。
class ActionDispatch::IntegrationTest
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
end
10.2 向夹具添加附件
您可以向您现有的夹具添加附件。首先,您需要创建一个单独的存储服务
# config/storage.yml
test_fixtures:
service: Disk
root: <%= Rails.root.join("tmp/storage_fixtures") %>
这告诉 Active Storage 将夹具文件“上传”到哪里,因此它应该是一个临时目录。通过将其与常规test
服务区分开,您可以将夹具文件与测试期间上传的文件区分开来。
接下来,为 Active Storage 类创建夹具文件
# active_storage/attachments.yml
david_avatar:
name: avatar
record: david (User)
blob: david_avatar_blob
# active_storage/blobs.yml
david_avatar_blob: <%= ActiveStorage::FixtureSet.blob filename: "david.png", service_name: "test_fixtures" %>
然后将一个文件放在您的夹具目录中(默认路径是test/fixtures/files
),并使用相应的文件名。有关更多信息,请参阅ActiveStorage::FixtureSet
文档。
一切设置好后,您就可以在测试中访问附件了
class UserTest < ActiveSupport::TestCase
def test_avatar
avatar = users(:david).avatar
assert avatar.attached?
assert_not_nil avatar.download
assert_equal 1000, avatar.byte_size
end
end
10.2.1 清理夹具
虽然测试中上传的文件会在每次测试结束时清理,但您只需要清理夹具文件一次:所有测试完成后。
如果您正在使用并行测试,请调用parallelize_teardown
class ActiveSupport::TestCase
# ...
parallelize_teardown do |i|
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
# ...
end
如果您没有运行并行测试,请使用Minitest.after_run
或您的测试框架的等效项(例如,RSpec 的after(:suite)
)
# test_helper.rb
Minitest.after_run do
FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)
end
10.3 配置服务
您可以添加config/storage/test.yml
来配置在测试环境中使用的服务。当使用service
选项时,这很有用。
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
没有config/storage/test.yml
,即使在运行测试时,也会使用在config/storage.yml
中配置的s3
服务。
将使用默认配置,文件将上传到在config/storage.yml
中配置的服务提供商。
在这种情况下,您可以添加config/storage/test.yml
,并对s3
服务使用 Disk 服务来防止发送请求。
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
s3:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
11 实现对其他云服务的支持
如果您需要支持除这些之外的云服务,则需要实现 Service。每个服务都通过实现将文件上传和下载到云所需的必要方法来扩展ActiveStorage::Service
。
12 清除未附加的上传
在某些情况下,文件会被上传但从未附加到记录。这在使用直接上传时可能会发生。您可以使用未附加范围查询未附加的记录。下面是一个使用自定义 rake 任务的示例。
namespace :active_storage do
desc "Purges unattached Active Storage blobs. Run regularly."
task purge_unattached: :environment do
ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge_later)
end
end
ActiveStorage::Blob.unattached
生成的查询可能会很慢,并且可能在具有较大数据库的应用程序中造成干扰。