更多内容请访问 rubyonrails.org:

Active Storage 概述

本指南介绍了如何将文件附加到 Active Record 模型。

阅读本指南后,您将了解

  • 如何将一个或多个文件附加到记录。
  • 如何删除附加文件。
  • 如何链接到附加文件。
  • 如何使用变体转换图像。
  • 如何生成非图像文件(例如 PDF 或视频)的图像表示。
  • 如何将文件直接从浏览器上传到存储服务,绕过应用程序服务器。
  • 如何清理测试期间存储的文件。
  • 如何实现对其他存储服务的支持。

1. 什么是 Active Storage?

Active Storage 便于将文件上传到 Amazon S3 或 Google Cloud Storage 等云存储服务,并将这些文件附加到 Active Record 对象。它为开发和测试提供了一个基于本地磁盘的服务,并支持将文件镜像到从属服务以进行备份和迁移。

使用 Active Storage,应用程序可以转换图像上传或生成非图像上传(如 PDF 和视频)的图像表示,并从任意文件中提取元数据。

1.1. 要求

Active Storage 的各种功能依赖于 Rails 不会安装的第三方软件,必须单独安装

与 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_blobsactive_storage_attachmentsactive_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 服务。对于您的应用程序使用的每个服务,提供一个名称和必要的配置。下面的示例声明了三个名为 localtestamazon 的服务

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 %>

继续阅读以获取有关内置服务适配器(例如 DiskS3)及其所需配置的更多信息。

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:ListBuckets3:PutObjects3:GetObjects3:DeleteObject公共访问 还需要 s3:PutObjectAcl。如果您配置了其他上传选项(例如设置 ACL),则可能需要额外的权限。

如果您想使用环境变量、标准 SDK 配置文件、配置文件、IAM 实例配置文件或任务角色,则可以省略上面示例中的 access_key_idsecret_access_keyregion 键。S3 服务支持 AWS SDK 文档 中描述的所有身份验证选项。

要连接到 DigitalOcean Spaces 等 S3 兼容对象存储 API,请提供 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. Google Cloud Storage 服务

config/storage.yml 中声明一个 Google Cloud Storage 服务

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://#/o/oauth2/auth"
    token_uri: "https://#/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"

可选地在签署 URL 时使用 IAM 而不是 credentials。这对于使用工作负载身份验证您的 GKE 应用程序很有用,有关更多信息,请参阅 这篇 Google Cloud 博客文章

google:
  service: GCS
  ...
  iam: true

可选地在签署 URL 时使用特定的 GSA。当使用 IAM 时,将联系 元数据服务器 以获取 GSA 电子邮件,但此元数据服务器并不总是存在(例如本地测试),您可能希望使用非默认 GSA。

google:
  service: GCS
  ...
  iam: true
  gsa_email: "foobar@baz.iam.gserviceaccount.com"

google-cloud-storage gem 添加到您的 Gemfile

gem "google-cloud-storage", "~> 1.11", require: false

2.4. 镜像服务

您可以通过定义镜像服务来使多个服务保持同步。镜像服务在两个或多个从属服务之间复制上传和删除操作。

镜像服务旨在在生产环境中服务之间迁移期间临时使用。您可以开始镜像到新服务,将旧服务中预先存在的文件复制到新服务,然后完全切换到新服务。

镜像不是原子操作。上传可能在主服务上成功,但在任何从属服务上失败。在完全切换到新服务之前,请验证所有文件是否已复制。

如上所述定义您要镜像的每个服务。在定义镜像服务时按名称引用它们

# 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.5. 公共访问

默认情况下,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 S3Google Cloud Storage 存储服务启用公共读取权限的文档。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

您可以通过在生成的附加对象上调用 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 相同,通过在生成的附加对象上调用 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 对象和文件名的哈希

@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: falsecontent_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 参数,则必须确保 key 对于上传来说是唯一的。建议在文件名后附加一个唯一的随机 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. 查询

Active Storage 附件在幕后是 Active Record 关联,因此您可以使用常规的 查询方法 来查找符合特定条件的附件记录。

4.1. has_one_attached

has_one_attached 创建一个名为 "<name>_attachment"has_one 关联和一个名为 "<name>_blob"has_one :through 关联。要选择头像为 PNG 的所有用户,请运行以下命令

User.joins(:avatar_blob).where(active_storage_blobs: { content_type: "image/png" })

4.2. has_many_attached

has_many_attached 创建一个名为 "<name>_attachments"has_many 关联和一个名为 "<name>_blobs"has_many :through 关联(注意复数形式)。要选择所有图像是视频而不是照片的消息,您可以执行以下操作

Message.joins(:images_blobs).where(active_storage_blobs: { content_type: "video/mp4" })

查询将基于 ActiveStorage::Blob 进行过滤,而不是 附件记录,因为这些是纯 SQL 连接。您可以将上述 blob 谓词与任何其他范围条件组合,就像使用任何其他 Active Record 查询一样。

5. 删除文件

要从模型中删除附件,请在附件上调用 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

6. 提供文件

Active Storage 支持两种提供文件的方式:重定向和代理。

所有 Active Storage 控制器默认都是可公开访问的。生成的 URL 难以猜测,但本质上是永久的。如果您的文件需要更高级别的保护,请考虑实现 经过身份验证的控制器

6.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 解耦,并允许例如在不同服务中镜像附件以实现高可用性。重定向的 HTTP 过期时间为 5 分钟。

要创建下载链接,请使用 rails_blob_{path|url} 帮助器。使用此帮助器可以设置 disposition。

rails_blob_path(user.avatar, disposition: "attachment")

为了防止 XSS 攻击,Active Storage 强制某些类型的文件将 Content-Disposition 标头设置为“attachment”。要更改此行为,请参阅 配置 Rails 应用程序 中可用的配置选项。

如果您需要在控制器/视图上下文之外(后台作业、Cronjobs 等)创建链接,您可以像这样访问 rails_blob_path

Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)

6.2. 代理模式

可选地,文件可以被代理。这意味着您的应用程序服务器将响应请求从存储服务下载文件数据。这对于从 CDN 提供文件很有用。

您可以将 Active Storage 配置为默认使用代理

# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy

或者,如果您想显式代理特定附件,可以使用 URL 帮助器 rails_storage_proxy_pathrails_storage_proxy_url

<%= image_tag rails_storage_proxy_path(@user.avatar) %>

6.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])) %>

6.3. 经过身份验证的控制器

所有 Active Storage 控制器默认都是可公开访问的。生成的 URL 使用一个普通的 signed_id,这使得它们难以猜测但永久。任何知道 blob URL 的人都可以访问它,即使您的 ApplicationController 中的 before_action 另有要求登录。如果您的文件需要更高级别的保护,您可以实现自己的经过身份验证的控制器,基于 ActiveStorage::Blobs::RedirectControllerActiveStorage::Blobs::ProxyControllerActiveStorage::Representations::RedirectControllerActiveStorage::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 进行访问。

7. 下载文件

有时您需要在上传 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 中可用。

8. 分析文件

Active Storage 在文件上传后通过将作业排队到 Active Job 来分析文件。分析的文件将在元数据哈希中存储额外信息,包括 analyzed: true。您可以通过调用 analyzed? 来检查 blob 是否已分析。

图像分析提供 widthheight 属性。视频分析提供这些属性,以及 durationangledisplay_aspect_ratio 以及指示这些通道是否存在​​的 videoaudio 布尔值。音频分析提供 durationbit_rate 属性。

9. 显示图像、视频和 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。您也可以直接调用这些方法。

9.1. 惰性加载与即时加载

默认情况下,Active Storage 将惰性处理表示。这段代码

image_tag file.representation(resize_to_limit: [100, 100])

将生成一个 <img> 标签,其 src 指向 ActiveStorage::Representations::RedirectController。浏览器将向该控制器发出请求,该控制器将执行以下操作

  1. 如果需要,处理文件并上传处理后的文件。
  2. 返回 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

9.2. 转换图像

转换图像允许您以所选的尺寸显示图像。要创建图像的变体,请在附件上调用 variant。您可以将变体处理器支持的任何转换传递给该方法。当浏览器访问变体 URL 时,Active Storage 将惰性地将原始 blob 转换为指定的格式并重定向到其新的服务位置。

<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>

如果请求变体,Active Storage 将根据图像的格式自动应用转换

  1. 可变内容类型(由 config.active_storage.variable_content_types 指示)且不被视为网络图像(由 config.active_storage.web_image_content_types 指示)将被转换为 PNG。

  2. 如果未指定 quality,将使用变体处理器对该格式的默认质量。

Active Storage 可以使用 Vips 或 MiniMagick 作为变体处理器。默认值取决于您的 config.load_defaults 目标版本,并且可以通过设置 config.active_storage.variant_processor 来更改处理器。

9.3. 预览文件

某些非图像文件可以预览:也就是说,它们可以作为图像呈现。例如,可以通过提取视频文件的第一帧来预览它。Active Storage 开箱即用支持预览视频和 PDF 文档。要创建指向惰性生成预览的链接,请使用附件的 preview 方法

<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>

要添加对其他格式的支持,请添加您自己的预览器。有关更多信息,请参阅 ActiveStorage::Preview 文档。

10. 直接上传

Active Storage 及其包含的 JavaScript 库支持从客户端直接上传到云端。

10.1. 用法

  1. 将 Active Storage JavaScript 包含在您的应用程序的 JavaScript 包中或直接引用它。

    直接引入,不通过应用 HTML 中的资产管道打包,带自动启动功能

    <%= javascript_include_tag "activestorage" %>
    

    通过 importmap-rails 引入,不通过应用 HTML 中的资产管道打包,不带自动启动功能,作为 ESM

    # config/importmap.rb
    pin "@rails/activestorage", to: "activestorage.esm.js"
    
    <script type="module-shim">
      import * as ActiveStorage from "@rails/activestorage"
      ActiveStorage.start()
    </script>
    

    使用资产管道

    //= require activestorage
    

    使用 npm 包

    import * as ActiveStorage from "@rails/activestorage"
    ActiveStorage.start()
    
  2. 使用 Rails 的 文件字段助手 为文件输入添加直接上传 URL 注释。

    <%= form.file_field :attachments, multiple: true, direct_upload: true %>
    

    或者,如果您没有使用 FormBuilder,请直接添加数据属性

    <input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
    
  3. 配置第三方存储服务上的 CORS 以允许直接上传请求。

  4. 就是这样!上传在表单提交时开始。

10.2. 跨域资源共享 (CORS) 配置

要使直接上传到第三方服务工作,您需要配置该服务以允许您的应用程序的跨域请求。请查阅您服务的 CORS 文档

注意允许

  • 您的应用可访问的所有来源
  • PUT 请求方法
  • 以下标头
    • Content-Type
    • Content-MD5
    • Content-Disposition
    • Cache-Control(对于 GCS,仅当设置了 cache_control 时)

磁盘服务不需要 CORS 配置,因为它共享您应用程序的来源。

10.2.1. 示例:S3 CORS 配置

[
  {
    "AllowedHeaders": [
      "Content-Type",
      "Content-MD5",
      "Content-Disposition"
    ],
    "AllowedMethods": [
      "PUT"
    ],
    "AllowedOrigins": [
      "https://www.example.com"
    ],
    "MaxAgeSeconds": 3600
  }
]

10.2.2. 示例:Google Cloud Storage CORS 配置

[
  {
    "origin": ["https://www.example.com"],
    "method": ["PUT"],
    "responseHeader": ["Content-Type", "Content-MD5", "Content-Disposition"],
    "maxAgeSeconds": 3600
  }
]

10.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> 所有直接上传都已结束。

10.4. 示例

您可以使用这些事件来显示上传进度。

direct-uploads

在表单中显示已上传的文件

// 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;
}

10.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)
    }
  })
}

10.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
  }
}

10.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

使用 直接上传 有时会导致文件上传成功,但从未附加到记录。请考虑 清除未附加的上传

11. 测试

使用 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

11.1. 丢弃测试期间创建的文件

11.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

11.1.2. 集成测试

与系统测试类似,在集成测试期间上传的文件不会自动清理。如果您想清除文件,可以在 teardown 回调中进行。

class ActionDispatch::IntegrationTest
  def after_teardown
    super
    FileUtils.rm_rf(ActiveStorage::Blob.service.root)
  end
end

如果您正在使用 并行测试 和磁盘服务,您应该配置每个进程使用自己的文件夹来存储 Active Storage。这样,teardown 回调将只删除相关进程测试中的文件。

class ActionDispatch::IntegrationTest
  parallelize_setup do |i|
    ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
  end
end

11.2. 将附件添加到夹具

您可以将附件添加到现有的 夹具。首先,您需要创建一个单独的存储服务

# config/storage.yml

test_fixtures:
  service: Disk
  root: <%= Rails.root.join("tmp/storage_fixtures") %>

这告诉 Active Storage 将夹具文件“上传”到何处,因此它应该是一个临时目录。通过使其与常规 test 服务不同,您可以将夹具文件与测试期间上传的文件分开。

接下来,为 Active Storage 类创建夹具文件

# test/fixtures/active_storage/attachments.yml
david_avatar:
  name: avatar
  record: david (User)
  blob: david_avatar_blob
# test/fixtures/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

11.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

11.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 服务用于磁盘服务,以防止发送请求。

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

s3:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

12. 实现对其他云服务的支持

如果您需要支持除这些服务之外的云服务,您将需要实现 Service。每个服务都通过实现将文件上传和下载到云所需的 方法来扩展 ActiveStorage::Service

13. 清理未附加的上传

在某些情况下,文件已上传但从未附加到记录。这在使用 直接上传 时可能会发生。您可以使用 unattached scope 查询未附加的记录。下面是一个使用 自定义 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 生成的查询可能很慢,并可能对具有大型数据库的应用程序造成干扰。



回到顶部