v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

Active Storage 概述

本指南介紹瞭如何將檔案附加到 Active Record models。

閱讀本指南後,您將瞭解:

1 Active Storage 是什麼?

Active Storage 有助於將檔案上傳到雲端儲存服務,例如 Amazon S3、Google Cloud Storage 或 Microsoft Azure Storage 並附加這些 檔案到 Active Record 物件。它帶有基於本地磁碟的服務,用於 開發測試,支援映象檔案到下級服務 備份和 migrations。

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

Active Storage 使用應用程式資料庫中名為的三個表 active_storage_blobs, active_storage_variant_recordsactive_storage_attachments。建立新的後 應用程式(或將您的應用程序升級到 Rails 5.2),執行 bin/rails active_storage:install 生成一個 migration 來建立這些 表。使用 bin/rails db:migrate 執行 migration。

警告:active_storage_attachments 是一個多型連線表,用於儲存您的 model 的類名。如果您的 model 的類名更改,您將需要在此表上執行 migration 以將基礎 record_type 更新為您的 model 的新類名。

警告:如果您在 models 上使用 UUID 而不是整數作為主要 key,則需要相應地更改生成的 migration 中 active_storage_attachments.record_idactive_storage_variant_records.id 的列型別。

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: "" # e.g. 'us-east-1'

透過設定告訴 Active Storage 使用哪個服務 Rails.application.config.active_storage.service。因為每個環境都會 可能使用不同的服務,建議在 每個環境的基礎。要使用前面示例中的磁碟服務 開發環境,您將新增以下內容 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

繼續閱讀有關內建服務介面卡的更多資訊(例如 DiskS3)以及它們需要的配置。

特定於環境的配置檔案將優先: 在生產中,例如,config/storage/production.yml 檔案(如果存在) 將優先於 config/storage.yml 檔案。

建議在bucket名稱中使用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 %>

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 服務:

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' or 'AES256'

提示:為您的應用程式設定合理的客戶端 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_key、 和 region keys 在上面的例子中。 S3 服務支援所有 AWS SDK 文件 中描述的身份驗證選項。

要連線到 S3 相容的物件儲存 API,例如 DigitalOcean Spaces,請提供 endpoint

digitalocean:
  service: S3
  endpoint: https://nyc3.digitaloceanspaces.com
  access_key_id: ...
  secret_access_key: ...
  # ...and other options

還有許多其他選項可用。您可以在 AWS S3 客戶端 文件中檢視它們。

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", require: false

2.4 谷歌雲端儲存服務

config/storage.yml 中宣告 Google Cloud Storage 服務:

google:
  service: GCS
  credentials: <%= Rails.root.join("path/to/keyfile.json") %>
  project: ""
  bucket: ""

可以選擇提供憑據雜湊而不是 keyfile 路徑:

google:
  service: GCS
  credentials:
    type: "service_account"
    project_id: ""
    private_ZHTW_30_WTHZ_id: <%= Rails.application.credentials.dig(:gcs, :private_ZHTW_30_WTHZ_id) %>
    private_ZHTW_30_WTHZ: <%= Rails.application.credentials.dig(:gcs, :private_ZHTW_30_WTHZ).dump %>
    client_email: ""
    client_id: ""
    auth_uri: "https://accounts.google.com/o/oauth2/auth"
    ZHTW_25_WTHZ_uri: "https://accounts.google.com/o/oauth2/ZHTW_25_WTHZ"
    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
    client_x509_cert_url: ""
  project: ""
  bucket: ""

Optionally provide a Cache-Control metadata to set on uploaded assets:

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 時,將聯絡 元資料伺服器 以獲取 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.5 映象服務

您可以透過定義映象服務來保持多個服務同步。一面鏡子 服務跨兩個或多個從屬服務複製上傳和刪除。

映象服務的目標在於在 migration 之間臨時使用 生產中的服務。您可以開始映象到新服務,複製 從舊服務到新服務的預先存在的檔案,然後在新服務上全力以赴 服務。

映象不是原子的。上傳成功是有可能的 主要服務並在任何從屬服務上失敗。去之前 全在新服務上,驗證所有檔案都已複製。

如上所述定義要映象的每個服務。參考 定義映象服務時按名稱命名:

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_keyfile.json") %>
  bucket: ""

public_gcs:
  <<: *gcs
  credentials: <%= Rails.root.join("path/to/public_keyfile.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 model。如果您希望每個使用者 有一個頭像,定義User model如下:

class User < ApplicationRecord
  has_one_attached :avatar
end

或者,如果您使用的是 Rails 6.0+,則可以像這樣執行 model 生成器命令:

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][Attached::One#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: "100x100"
  end
end

呼叫 avatar.variant(:thumb) 獲取頭像的拇指變體:

<%= image_tag user.avatar.variant(:thumb) %>

3.2 has_many_attached

has_many_attached 宏在記錄之間建立一對多關係 和檔案。每條記錄可以附加許多檔案。

例如,假設您的應用程式有一個 Message model。如果你想要每個 訊息有很多影象,定義 Message model 如下:

class Message < ApplicationRecord
  has_many_attached :images
end

或者,如果您使用的是 Rails 6.0+,則可以像這樣執行 model 生成器命令:

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][Attached::Many#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: "100x100"
  end
end

3.3 附加檔案/IO 物件

有時您需要附加一個不是透過 HTTP 請求到達的檔案。 例如,您可能希望附加在磁碟上生成或下載的檔案 來自使用者提交的 URL。您可能還想在一個資料夾中附加一個夾具檔案 model 測試。為此,請提供至少包含一個開放 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。

4 刪除檔案

要從 model 中刪除附件,請呼叫 [purge][Attached::One#purge] 依戀。如果您的應用程式設定為使用 Active Job,則可以完成刪除 在後臺呼叫 [purge_later][Attached::One#purge_later]。 清除會從儲存服務中刪除 Blob 和檔案。

# 同步銷燬頭像和實際資原始檔。
user.avatar.purge

# 透過 Active Job 非同步銷燬關聯的 models 和實際資原始檔。
user.avatar.purge_later

5 提供檔案

Active Storage 支援兩種檔案服務方式:重定向和代理。

警告:預設情況下,所有 Active Storage controllers 都是公開訪問的。這 生成的 URL 很難猜測,但在設計上是永久的。如果你的檔案 需要更高級別的保護考慮實施 經過身份驗證的 Controllers

5.1 重定向模式

要為 blob 生成永久 URL,您可以將 blob 傳遞給 url_for view helper。這會產生一個 帶有 blob signed_id 的 URL 路由到 blob 的 RedirectController

url_for(user.avatar)
# => /rails/active_storage/blobs/:signed_id/my-avatar.png

RedirectController 重定向到實際的服務端點。這 間接將服務 URL 與實際 URL 分離,並允許 例如,映象不同服務中的附件以獲得高可用性。這 重定向的 HTTP 過期時間為 5 分鐘。

要建立下載連結,請使用 rails_blob_{path|url} helper。使用這個 helper 允許您設定處置。

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

警告:為了防止 XSS 攻擊,Active Storage 強制使用 Content-Disposition 標頭 對某種檔案進行“附件”。要更改此行為,請參閱 設定 Rails 應用程式 中的可用設定選項。

如果您需要從 controller/view 上下文(背景 作業、Cronjobs 等),您可以像這樣訪問 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 helpers。

<%= image_tag rails_storage_proxy_path(@user.avatar) %>
5.2.1 在 Active Storage 前面放一個 CDN

此外,為了對 Active Storage 附件使用 CDN,您需要生成具有代理模式的 URL,以便它們由您的應用程式提供服務,並且 CDN 將快取附件而無需任何額外設定。這是開箱即用的,因為預設的 Active Storage 代理 controller 設定了一個 HTTP 標頭,指示 CDN 快取回應。

您還應該確保生成的 URL 使用 CDN 主機而不是您的應用程式主機。有多種方法可以實現這一點,但總的來說,它涉及調整 config/routes.rb 檔案,以便您可以為附件及其變體生成正確的 URL。例如,您可以新增以下內容:

# 設定/路由.rb
direct :cdn_image do |model, options|
  if model.respond_to?(:signed_id)
    route_for(
      :rails_service_blob_proxy,
      model.signed_id,
      model.filename,
      options.merge(host: ENV['CDN_HOST'])
    )
  else
    signed_blob_id = model.blob.signed_id
    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 已認證的 Controller

預設情況下,所有 Active Storage controllers 都是公開訪問的。生成的 URL 使用普通的 signed_id,這使得它們難以 猜測但永久。任何知道 blob URL 的人都可以訪問它, 即使您的 ApplicationController 中的 before_action 否則 需要登入。如果您的檔案需要更高級別的保護,您可以 實現你自己的認證 controllers,根據 ActiveStorage::Blobs::RedirectController, ActiveStorage::Blobs::ProxyController, ActiveStorage::Representations::RedirectControllerActiveStorage::Representations::ProxyController

要僅允許帳戶訪問其自己的徽標,您可以執行以下操作:

# 設定/路由.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 callback 中尚不可用,但僅在 after_create_commit 中可用。

7 分析檔案

Active Storage 透過在 Active Job 中排隊作業來分析上傳的檔案。分析的檔案將在元資料雜湊中儲存附加資訊,包括 analyzed: true。您可以透過呼叫 analyzed? 來檢查是否已分析 blob。

影象分析提供 widthheight 屬性。影片分析提供這些以及 durationangledisplay_aspect_ratiovideoaudio 布林值來指示這些頻道的存在。音訊分析提供 durationbit_rate 屬性。

8 顯示影象、影片和 PDF

Active Storage 支援表示多種檔案。你可以打電話 representation 在附件上顯示影象變體,或 影片或 PDF 的 preview。在呼叫 representation 之前,檢查是否 附件可以透過呼叫 representable? 來表示。一些檔案格式 開箱即用的 Active Storage 無法預先處理 view(例如 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 previewable 檔案。您也可以直接呼叫這些方法。

8.1 延遲載入與立即載入

預設情況下,Active Storage 將延遲處理表示。這段程式碼:

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

將生成一個 <img> 標籤,其中 src 指向 ActiveStorage::Representations::RedirectController。瀏覽器會 向該 controller 發出請求,它將返回一個 302 重定向到 遠端服務上的檔案(或在[代理模式](#proxy-mode)下,返回檔案 內容)。延遲載入檔案允許以下功能 單次使用 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 規定) 並且不考慮網路影象(由 config.active_storage.web_image_content_types 規定), 將被轉換為 PNG。

  2. 如果未指定 quality,則將使用變體處理器的格式預設質量。

Active Storage 的預設處理器是 MiniMagick,但您也可以使用 貴賓。要切換到 Vips,請將以下內容新增到 config/application.rb

config.active_storage.variant_processor = :vips

兩個處理器不完全相容,因此在遷移現有應用程式時 使用 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 Previewing 檔案

一些非影象檔案可以是 previewed:也就是說,它們可以作為影象呈現。 例如,影片檔案可以透過提取其第一幀來 previewed。在......之外 盒子,Active Storage 支援 previewing 影片和 PDF 文件。去創造 一個指向延遲生成的 preview 的連結,使用附件的 preview 方法:

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

要新增對其他格式的支援,請新增您自己的 previewer。見 ActiveStorage::Preview 文件瞭解更多資訊。

9 直接上傳

Active Storage,自帶JavaScript庫,支援上傳 直接從客戶端到雲端。

9.1 用法

  1. 在應用程式的 JavaScript 包中包含 activestorage.js

    使用 asset pipeline:

    //= 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,允許直接上傳請求。

  1. 就是這樣!上傳在表單提交時開始。

9.2 跨域資源共享(CORS)設定

要直接上傳到第三方服務,您需要設定該服務以允許來自您的應用的跨源請求。查閱 CORS 文件以瞭解您的服務:

注意允許:

  • 訪問您的應用程式的所有來源
  • PUT 請求方法
  • 以下標題:
    • Origin
    • 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": [
      "*"
    ],
    "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 儲存 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.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 與庫或框架整合

如果您想使用 JavaScript 框架中的直接上傳功能,或者 您想整合自定義拖放解決方案,您可以使用 DirectUpload 類用於此目的。從您的圖書館收到檔案後 選擇實例化 DirectUpload 並呼叫其建立方法。建立鏡頭 上傳完成時呼叫的 callback。

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

如果需要跟蹤檔案上傳的進度,可以透過第三個 DirectUpload 建構函式的引數。在上傳過程中,DirectUpload 將呼叫物件的 directUploadWillStoreFileWithXHR 方法。然後你可以 在 XHR 上繫結您自己的進度處理程式。

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) {
        // 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 測試

使用 fixture_file_upload 在整合或 controller 測試中測試上傳檔案。 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 系統測試

系統測試透過回滾 transaction 來清理測試資料。因為destroy 永遠不會在物件上呼叫,因此永遠不會清除附加的檔案。如果你 要清除檔案,您可以在 after_teardown callback 中進行。正在做 此處確保測試期間建立的所有連線都完整且 您不會收到來自 Active Storage 的錯誤訊息,說它找不到檔案。

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  # ...
  def after_teardown
    super
    FileUtils.rm_rf(ActiveStorage::Blob.service.root)
  end
  # ...
end

如果你使用 [parallel tests][] 和 DiskService,你應該設定每個程序使用它自己的 Active Storage 的資料夾。這樣,teardown callback 只會從相關程序中刪除檔案' 測試。

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

如果您的系統測試驗證了 model 與附件的刪除,並且您 使用 Active Job,將您的測試環境設定為使用內聯佇列介面卡,以便 清除作業會立即執行,而不是在未來的未知時間執行。

# 使用內聯作業處理使事情立即發生
config.active_job.queue_adapter = :inline
10.1.2 整合測試

與系統測試類似,整合測試期間上傳的檔案不會被 自動清理。如果要清除檔案,可以在 teardown callback。

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

如果您使用 [parallel tests][] 和 Disk 服務,您應該設定每個程序使用它自己的 Active Storage 的資料夾。這樣,teardown callback 只會從相關程序中刪除檔案' 測試。

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

10.2 為燈具新增附件

您可以將附件新增到現有的 [裝置][]。首先,您需要建立一個單獨的儲存服務:

# 設定/儲存.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" %>

然後在你的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

11 實施對其他雲服務的支援

如果您需要支援除這些之外的雲服務,您將需要 實施服務。每項服務延伸 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("active_storage_blobs.created_at <= ?", 2.days.ago).find_each(&:purge_later)
  end
end

警告:由 ActiveStorage::Blob.unattached 生成的查詢可能很慢,並且可能會對具有較大資料庫的應用程式造成破壞。

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區