edge
更多信息请访问 rubyonrails.org: 更多 Ruby on Rails

Active Storage概述

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

阅读本指南后,您将了解以下内容:

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 一个多态连接表,连接您的模型和blobs。如果您的模型类名更改了,您需要在此表上运行迁移,以更新底层的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") %>

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  bucket: ""
  region: "" # 例如 'us-east-1'

通过设置Rails.application.config.active_storage.service告诉Active Storage使用哪个服务。因为每个环境可能使用不同的服务,建议在每个环境上进行设置。要在开发环境中使用前面示例中的磁盘服务,您需要将以下内容添加到config/environments/development.rb

# 将文件存储在本地。
config.active_storage.service = :local

要在生产环境中使用S3服务,您需要将以下内容添加到config/environments/production.rb

# 将文件存储在Amazon S3上。
config.active_storage.service = :amazon

要在测试时使用测试服务,您需要将以下内容添加到config/environments/test.rb

# 将上传的文件存储在本地文件系统的临时目录中。
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 Disk 服务

config/storage.yml 中声明一个 Disk 服务:

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

2.2 S3 服务(Amazon S3 和兼容 S3 API)

要连接到 Amazon S3,请在 config/storage.yml 中声明一个 S3 服务:

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""

可选地提供客户端和上传选项:

amazon:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""
  http_open_timeout: 0
  http_read_timeout: 0
  retry_limit: 0
  upload:
    server_side_encryption: "" # 'aws:kms' 或 '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: ...
  secret_access_key: ...
  # ...和其他选项

还有许多其他可用选项。您可以在 AWS S3 Client 文档中查看它们。

2.3 Microsoft Azure 存储服务

config/storage.yml 中声明一个 Azure 存储服务:

azure:
  service: AzureStorage
  storage_account_name: ""
  storage_access_key: ""
  container: ""

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: ""

可选地提供一个凭据的哈希而不是密钥文件路径:

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: ""

可选地提供一个 Cache-Control 元数据以设置上传的资源:

google:
  service: GCS
  ...
  cache_control: "public, max-age=3600"

如果要在签名 URL 时使用 IAM 而不是 credentials,可以选择使用。如果您正在使用 Workload Identity 对 GKE 应用程序进行身份验证,请参阅 此 Google Cloud 博客文章 了解更多信息。

google:
  service: GCS
  ...
  iam: true

如果要在签名 URL 时使用特定的 GSA,请使用 IAM。在使用 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 镜像服务

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

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

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

按照上面描述的方式定义要镜像的每个服务。在定义镜像服务时,使用名称引用它们:

s3_west_coast:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""

s3_east_coast:
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""

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: ""

public_gcs:
  <<: *gcs
  credentials: <%= Rails.root.join("path/to/public_key.json") %>
  bucket: ""
  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.require(:user).permit(: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: :s3
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) %>

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.require(:message).permit(: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

3.3 附加文件/IO对象

有时您需要附加一个不是通过HTTP请求到达的文件。例如,您可能希望附加一个在磁盘上生成的文件或从用户提交的URL下载的文件。您还可能希望在模型测试中附加一个固定文件。为此,请提供一个包含至少一个打开的IO对象和一个文件名的哈希:

@message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf')

在可能的情况下,还提供内容类型。Active Storage尝试从数据中确定文件的内容类型。如果无法确定,它将使用您提供的内容类型作为后备。 ruby @message.images.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')

您可以通过在content_type参数中传入identify: false来绕过数据的内容类型推断。

@message.images.attach(
  io: File.open('/path/to/file'),
  filename: 'file.pdf',
  content_type: 'application/pdf',
  identify: false
)

如果您没有提供内容类型,并且Active Storage无法自动确定文件的内容类型,则默认为application/octet-stream

4 删除文件

要从模型中删除附件,请在附件上调用purge方法。如果您的应用程序设置为使用Active Job,则可以通过调用purge_later方法在后台进行删除。清除操作会从存储服务中删除blob和文件。

# 同步销毁头像和实际资源文件。
user.avatar.purge

# 通过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)
# => /rails/active_storage/blobs/: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

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

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

5.2.1 在Active Storage前面放置CDN

此外,为了在Active Storage附件中使用CDN,您需要生成带有代理模式的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::RedirectControllerActiveStorage::Representations::ProxyController实现自己的身份验证控制器。

要仅允许帐户访问其自己的标志,可以执行以下操作: ```ruby

config/routes.rb

resource :account do resource :logo end ```

# app/controllers/logos_controller.rb
class LogosController < ApplicationController
  # 通过 ApplicationController:
  # 包括 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_ratiovideoaudio布尔值来指示这些通道的存在。音频分析提供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])

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

  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来更改处理器。

这两个处理器不完全兼容,因此在现有应用程序之间迁移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 }) %>

可用的参数由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在其自己的文档中有更多可用选项(例如saver,允许配置图像压缩)的信息,适用于VipsMiniMagick处理器。

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. file字段中添加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请求方法
  • 以下标头:
    • Origin
    • Content-Type
    • Content-MD5
    • Content-Disposition(Azure Storage除外)
    • x-ms-blob-content-disposition(仅适用于Azure Storage)
    • x-ms-blob-type(仅适用于Azure Storage)
    • Cache-Control(仅适用于GCS,仅在设置了cache_control时) 由于磁盘服务与应用程序的源相同,因此不需要进行CORS配置。

9.2.1 示例:S3 CORS配置

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

9.2.2 示例:Google Cloud Storage CORS配置

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

9.2.3 示例:Azure Storage CORS配置

<Cors>
  <CorsRule>
    <AllowedOrigins>https://www.example.com</AllowedOrigins>
    <AllowedMethods>PUT</AllowedMethods>
    <AllowedHeaders>Origin, 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]')

// 绑定文件拖放 - 使用父元素的ondrop或使用Dropzone等库
const onDrop = (event) => {
  event.preventDefault()
  const files = event.dataTransfer.files;
  Array.from(files).forEach(file => uploadFile(file))
}

// 绑定普通文件选择
input.addEventListener('change', (event) => {
  Array.from(input.files).forEach(file => uploadFile(file))
  // 可以清除输入框中选择的文件
  input.value = null
})

const uploadFile = (file) => {
  // 表单需要设置file_field direct_upload: true,这样会提供data-direct-upload-url
  const url = input.dataset.directUploadUrl
  const upload = new DirectUpload(file, url)

  upload.create((error, blob) => {
    if (error) {
      // 处理错误
    } else {
      // 向表单添加一个适当命名的隐藏输入,其值为blob.signed_id,以便在正常上传流程中传输blob id
      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上。 ```js import { DirectUpload } from "@rails/activestorage"

class Uploader { constructor(file, url) { this.upload = new DirectUpload(this.file, this.url, this) }

upload(file) { this.upload.create((error, blob) => { if (error) { // 处理错误 } else { // 在表单中添加一个适当命名的隐藏输入,其值为blob.signed_id } }) }

directUploadWillStoreFileWithXHR(request) { request.upload.addEventListener("progress", event => this.directUploadDidProgress(event)) }

directUploadDidProgress(event) { // 使用event.loaded和event.total来更新进度条 } } ```

9.7 与库或框架集成

一旦从所选库中接收到文件,您需要创建一个DirectUpload实例,并使用其"create"方法来启动上传过程,根据需要添加任何所需的附加标头。 "create"方法还需要提供一个回调函数,一旦上传完成就会触发该函数。

import { DirectUpload } from "@rails/activestorage"

class Uploader {
  constructor(file, url, token) {
    const headers = { 'Authentication': `Bearer ${token}` }
    // 信息:发送标头是一个可选参数。如果选择不发送标头,身份验证将使用cookie或会话数据进行。
    this.upload = new DirectUpload(this.file, this.url, this, headers)
  }

  upload(file) {
    this.upload.create((error, blob) => {
      if (error) {
        // 处理错误
      } else {
        // 使用blob.signed_id作为下一个请求中的文件引用
      }
    })
  }

  directUploadWillStoreFileWithXHR(request) {
    request.upload.addEventListener("progress",
      event => this.directUploadDidProgress(event))
  }

  directUploadDidProgress(event) {
    // 使用event.loaded和event.total来更新进度条
  }
}

要实现自定义身份验证,必须在Rails应用程序上创建一个新的控制器,类似于以下内容:

class DirectUploadsController < ActiveStorage::DirectUploadsController
  skip_forgery_protection
  before_action :authenticate!

  def authenticate!
    @token = request.headers['Authorization']&.split&.last

    return head :unauthorized unless valid_token?(@token)
  end
end

注意:使用直接上传有时可能会导致上传的文件不会附加到记录上。考虑清除未附加的上传

10 测试

使用fixture_file_upload在集成或控制器测试中测试上传文件。Rails将文件处理为任何其他参数。

class SignupController < ActionDispatch::IntegrationTest
  test "can sign up" do
    post signup_path, params: {
      name: "David",
      avatar: fixture_file_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,请将测试环境设置为使用内联队列适配器,以便清理作业立即执行,而不是在未来的某个未知时间执行。

# 使用内联作业处理使事情立即发生
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类创建fixture文件:

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

然后在你的fixture目录中放置一个与文件名对应的文件。 有关更多信息,请参阅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 清理Fixture

虽然在测试中上传的文件会在每个测试结束时被清理, 但您只需要在所有测试完成时清理fixture文件一次。

如果您正在使用并行测试,请调用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 清除未附加的上传文件

有时文件被上传但从未附加到记录上。这可能发生在使用直接上传时。您可以使用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生成的查询可能在具有较大数据库的应用程序上变慢并且可能会造成干扰。

反馈

欢迎您帮助改进本指南的质量。

如果您发现任何拼写错误或事实错误,请贡献您的意见。 要开始,请阅读我们的 文档贡献 部分。

您还可能会发现不完整的内容或过时的内容。 请为主要内容添加任何缺失的文档。请先检查 Edge 指南,以验证问题是否已经修复或尚未修复。 请参阅 Ruby on Rails 指南准则 以了解样式和规范。

如果您发现需要修复但无法自行修复的问题,请 提交问题

最后但同样重要的是,欢迎您在 官方 Ruby on Rails 论坛 上讨论有关 Ruby on Rails 文档的任何问题。