更多内容请访问 rubyonrails.org:

Active Storage 概述

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

阅读本指南后,您将了解

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

1 什么是 Active Storage?

Active Storage 促进将文件上传到云存储服务(如 Amazon S3、Google Cloud Storage 或 Microsoft Azure Storage)并将这些文件附加到 Active Record 对象。它附带一个用于开发和测试的本地磁盘服务,并支持将文件镜像到下级服务以进行备份和迁移。

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

1.1 要求

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

图像分析和转换还需要 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_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 %>

azure:
  service: AzureStorage
  # ...
  container: your_container_name-<%= 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 文档 中描述的所有身份验证选项。

要连接到与 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 S3Google Cloud StorageMicrosoft 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_pathrails_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::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 访问文件。

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 是否已分析。

图像分析提供 widthheight 属性。视频分析提供这些属性,以及 durationangledisplay_aspect_ratio 以及 videoaudio 布尔值来指示这些通道的存在。音频分析提供 durationbit_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。浏览器将向该控制器发出请求,该控制器将执行以下操作

  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

8.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 决定)且不被视为 Web 图像(由 config.active_storage.web_image_content_types 决定),将被转换为 PNG。

  2. 如果未指定 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 在其自己的文档中提供了所有参数,适用于 VipsMiniMagick 处理器。

某些参数(包括上面列出的参数)接受额外的处理器特定选项,这些选项可以作为 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 用法

  1. 在您的应用程序的 JavaScript 包中包含 activestorage.js

    使用资产管道

    //= require activestorage
    

    使用 npm 包

    import * as ActiveStorage from "@rails/activestorage"
    ActiveStorage.start()
    
  2. direct_upload: true 添加到您的 文件字段

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

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

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

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

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

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

// 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生成的查询可能会很慢,并且可能在具有较大数据库的应用程序中造成干扰。



返回顶部