Active Storage の概要

このガイドはActive Recordモデルにファイルを添付する方法について説明します。

本ガイドの内容:

  • あるレコードに1個または複数のファイルを添付する方法
  • 添付ファイルを削除する方法
  • 添付ファイルへのリンク方法
  • バリアントを利用して画像を変形する方法
  • PDFや動画などの非画像ファイルの内容を代表するプレビュー画像の生成方法
  • ブラウザからストレージサービスに直接ファイルをアップロードする方法
  • テスト中に保存したファイルをクリーンアップする方法
  • 追加のストレージサービスをサポートするための実装方法

1 Active Storageについて

Active Storageは、Amazon S3、Google Cloud Storage、Microsoft Azure Storageなどのクラウドストレージサービスへのファイルのアップロードや、ファイルをActive Recordオブジェクトにアタッチする機能を提供します。 development環境とtest環境向けのローカルディスクベースのサービスを利用できるようになっており、ファイルを下位のサービスにミラーリングしてバックアップや移行に用いることも可能です。

Active Storageは、アプリケーションにアップロードした画像の変形や、PDFや動画などの画像以外のアップロードファイルの内容を代表する画像の生成、任意のファイルからのメタデータ抽出にも利用できます

1.1 要件

Active Storageの多くの機能は、Railsによってインストールされないサードパーティソフトウェアに依存しているため、別途インストールが必要です。

  • libvips v8.6以降(またはImageMagick): 画像解析や画像変形用
  • ffmpeg v3.4以降: 動画プレビュー、ffprobeによる動画/音声解析
  • popplerまたはmuPDF: PDFプレビュー用

画像分析や画像加工のためにimage_processing gemも必要です。Gemfileimage_processing gemをコメント解除するか、必要に応じて追加します。

gem "image_processing", ">= 1.2"

ImageMagickは、libvipsに比べて知名度が高く普及も進んでいます。しかしlibvipsは10倍高速かつメモリ消費も1/10です。JPEGファイルの場合、libjpeg-devlibjpeg-turbo-devに置き換えると2〜7倍高速になります。

サードパーティのソフトウェアをインストールして使う前に、そのソフトウェアのライセンスを読んで理解しておきましょう。特にMuPDFはAGPLでライセンスされており、利用目的によっては商用ライセンスが必要です。

2 セットアップ

Active Storageは、アプリケーションのデータベースで active_storage_blobsactive_storage_variant_recordsactive_storage_attachmentsという名前の3つのテーブルを使います。 新規アプリケーション作成した後(または既存のアプリケーションをRails 5.2にアップグレードした後)に、rails active_storage:installを実行して、これらのテーブルを作成するマイグレーションファイルを作成します。マイグレーションファイルを実行するにはrails db:migrateを使います。

active_storage_attachmentsは、使うモデルのクラス名を保存するポリモーフィックjoinテーブルです。モデルのクラス名を変更した場合は、このテーブルに対してマイグレーションを実行して背後のrecord_typeをモデルの新しいクラス名に更新する必要があります。

モデルの主キーに整数値ではなくUUIDを使っている場合は、生成されるマイグレーションファイルのactive_storage_attachments.record_idactive_storage_variant_records.idのカラム型も変更する必要があります。

Active Storageのサービスはconfig/storage.ymlで宣言します。アプリケーションが使うサービスごとに、名前と必要な構成を指定します。 次の例では、localtestamazonという3つのサービスを宣言しています。

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: "" # 例: 'ap-northeast-1'

利用するサービスをActive Storageに認識させるには、Rails.application.config.active_storage.serviceを設定します。 使うサービスは環境ごとに異なることもあるため、この設定を環境ごとに行うことをおすすめします。前述したローカルDiskサービスをdevelopment環境で使うには、config/environments/development.rbに以下を追加します。

# ファイルをローカルに保存する
config.active_storage.service = :local

production環境でAmazon S3を利用するには、config/environments/production.rbに以下を追加します。

# ファイルをAmazon S3に保存する
config.active_storage.service = :amazon

テスト時にテストサービスを利用するには、config/environments/test.rbに以下を追加します。

# ローカルファイルシステム上のアップロード済みファイルを一時ディレクトリに保存する
config.active_storage.service = :test

組み込みのサービスアダプタ(DiskS3など)およびそれらに必要な設定について、詳しくは後述します。

環境固有の設定ファイルが優先されます。たとえばproduction環境では、config/storage/production.ymlファイルが存在すればconfig/storage.ymlファイルよりも優先されます。

productionのデータ喪失リスクをさらに軽減するために、以下のようにバケット名に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 Diskサービス

Diskサービスはconfig/storage.ymlで宣言します。

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

2.2 Amazon S3サービス(およびS3互換API)

S3サービスはconfig/storage.ymlで宣言します。

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'

HTTPタイムアウトやリトライ上限数には、アプリケーションに適した値を設定してください。特定の障害シナリオでは、デフォルトのAWSクライアント設定によってコネクションが数分間保持されてしまい、リクエストの待ち行列が発生する可能性があります。

Gemfileaws-sdk-s3 gemを追加します。

gem "aws-sdk-s3", require: false

Active Storageのコア機能では、s3:ListBuckets3:PutObjects3:GetObjects3:DeleteObjectという4つのパーミッションが必要です。パブリックアクセスの場合はs3:PutObjectAclも必要です。ACLの設定といったアップロードオプションを追加で設定した場合は、この他にもパーミッションが必要になることがあります。

環境変数、標準SDKの設定ファイル、プロファイル、IAMインスタンスのプロファイルやタスクロールを使いたい場合は、上述のaccess_key_idsecret_access_keyregionを省略できます。Amazon S3サービスでは、AWS SDK documentationに記載されている認証オプションをすべてサポートします。

DigitalOcean SpacesなどのS3互換オブジェクトストレージAPIに接続するには、endpointを指定します。

digitalocean:
  service: S3
  endpoint: https://nyc3.digitaloceanspaces.com
  access_key_id: ...
  secret_access_key: ...
  # ...その他のオプション

この他にもさまざまなオプションが利用できます。AWS S3 Client APIドキュメントを参照してください。

2.3 Microsoft Azure Storageサービス

Azure Storageサービスはconfig/storage.ymlで宣言します。

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

Gemfileazure-storage-blob gemを追加します。

gem "azure-storage-blob", "~> 2.0", require: false

2.4 Google Cloud Storageサービス

Google Cloud Storageサービスはconfig/storage.ymlで宣言します。

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

keyfileパスの代わりにcredentialのハッシュも渡せます。

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に署名する場合に、credentialsの代わりにIAMをオプションで利用できます。これは、GKEアプリケーションをWorkload Identityで認証する場合に便利です。詳しくはGoogle Cloudのブログ記事『Introducing Workload Identity: Better authentication for your GKE applications』を参照してください。

google:
  service: GCS
  ...
  iam: true

オプションで、URLに署名するときに特定のGSAを使えます。IAMを使う場合は、GSAのメールを受け取るためにメタデータサーバーにアクセスしますが、このメタデータサーバーは常に存在するとは限らず(ローカルテスト時など)、デフォルト以外のGSAを使いたい場合もあります。

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

Gemfilegoogle-cloud-storage gemを追加します。

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

2.5 ミラーサービス

ミラーサービスを定義すると、複数のサービスを同期できます。ミラーサービスは、複数の下位サービスにアップロードや削除をレプリケーションします。

ミラーサービスは、production環境でサービス間の移行期で一時的に利用するための機能です。新しいサービスへのミラーリングを開始し、既存のファイルを古いサービスから新しいサービスにコピーしてから、新しいサービスに全面的に移行できます。

ミラーリング機能はアトミックではありません。プライマリサービスでアップロードに成功しても、サブサービスでは失敗する可能性があります。新しいサービスを開始する前に、すべてのファイルがコピー完了していることを確認してください。

上で説明したように、ミラーリングするサービスをそれぞれ定義します。ミラーサービスを定義するときは以下のように名前で参照します。

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マクロは、レコードとファイルの間に1対1のマッピングを設定します。レコード1件ごとに1個のファイルを添付できます。

たとえば、アプリケーションにUserモデルがあるとします。各userにアバター画像を添付したい場合は、以下のようにUserモデルを定義します。

class User < ApplicationRecord
  has_one_attached :avatar
end

Rails 6.0以降を使う場合は、以下のようにモデルのジェネレータコマンドを実行できます。

bin/rails generate model User avatar:attachment

以下のように書くことでアバター画像付きのuserを作成できます。

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

既存のuserにアバター画像を添付するにはavatar.attachを呼び出します。

user.avatar.attach(params[:avatar])

avatar.attached?で特定のuserがアバター画像を持っているかどうかを調べられます。

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

3.2 has_many_attached

has_many_attachedマクロは、レコードとファイルの間に1対多の関係を設定します。レコード1件ごとに、多数の添付ファイルを添付できます。

たとえば、アプリケーションにMessageモデルがあるとします。メッセージごとに多数の画像を持たせるには、次のようなMessageモデルを定義します.

class Message < ApplicationRecord
  has_many_attached :images
end

以下のように書くことで、画像付きのメッセージを作成できます。

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 File/IO Objectsをアタッチする

HTTPリクエスト経由では配信されないファイルをアタッチする必要が生じる場合があります。たとえば、ディスク上で生成したファイルやユーザーが送信したURLからダウンロードしたファイルをアタッチしたい場合や、モデルのテストでfixtureファイルをアタッチしたい場合などが考えられます。これを行うには、以下のようにopen IOオブジェクトとファイル名を1つ以上含むハッシュを渡します。

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

可能であれば、content_type:オプションも指定しておきましょう。Active Storageは、渡されたデータからファイルのContent-Typeの判定を試みますが、判定できない場合は指定のContent-Typeにフォールバックします。

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

以下のようにcontent_typeidentify: falseを渡すと、Content-Typeの推測をバイパスできます。

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

content_type:を指定せず、Active StorageがファイルのContent-Typeを自動的に判別できない場合は、デフォルトでapplication/octet-streamが設定されます。

4 ファイルを削除する

添付ファイルをモデルから削除するには、添付ファイルに対してpurgeを呼び出します。 Active Jobを使うようにアプリケーションが設定されている場合は、バックグラウンドで削除を実行できます。purgeすると、blobとファイルがストレージサービスから削除されます。

# avatarと実際のリソースファイルを同期的に破棄します。
user.avatar.purge

# Active Jobを介して、関連付けられているモデルと実際のリソースファイルを非同期で破棄します。
user.avatar.purge_later

5 ファイルを配信する

Active Storageは「リダイレクト」と「プロキシ」という2種類のファイル配信をサポートしています。

Active Storageのすべてのコントローラは、デフォルトでpublicアクセスできます。生成されるURLは推測が困難ですが、設計上は永続的なURLになります。ファイルをより高度なレベルで保護する必要がある場合は、認証済みコントローラの実装を検討してください。

5.1 リダイレクトモード

url_forビューヘルパーにblobを渡すと、永続的なblob URLを生成できます。生成されるURLでは、そのblobのRedirectControllerにルーティングされるsigned_idが使われます。

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

RedirectControllerは、サービスの実際のエンドポイントにリダイレクトします。この間接参照によってサービスURLと実際のURLが切り離され、たとえば添付ファイルを別サービスにミラーリングして可用性を高めることが可能になります。リダイレクトのHTTP有効期限は5分です。

ダウンロードリンクを作成するには、rails_blob_pathrails_blob_urlヘルパーを使います。このヘルパーではContent-Dispositionヘッダーを指定できます。

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

XSS(クロスサイトスクリプティング)攻撃を防ぐため、Active Storageは特定の種類のファイルについてContent-Dispositionヘッダーを強制的に"attachment"に設定します。この振る舞いを変更する場合は、Active Storageの設定方法で利用可能な設定オプションを参照してください。

バックグラウンドジョブやcronジョブなど、コントローラやビューのコンテキストの外でリンクを作成する必要がある場合は、以下のような方法でrails_blob_pathにアクセスできます。

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

5.2 プロキシモード

ファイルをプロキシ(proxy)することもオプションで可能です。この場合、リクエストのレスポンスで、アプリケーションサーバーがファイルデータをストレージサービスからダウンロードします。プロキシモードは、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 Active Storageの手前にCDNを配置する

Active Storageの添付ファイルでCDNを使うには、URLをプロキシモードで生成してアプリで提供し、CDNで追加設定を行わずに添付ファイルがCDNでキャッシュされるようにする必要があります。Active Storageのデフォルトのプロキシコントローラは、レスポンスをキャッシュするようにCDNに指示するHTTPヘッダーを設定するので、すぐに利用できます。

また、生成されるURLがアプリのホストではなくCDNのホストを使うようにする必要もあります。これを行う方法は複数ありますが、一般にはアプリのconfig/routes.rbファイルを調整して、添付ファイルやそのバリエーションのURLが正しく生成されるようにします。たとえば以下を追加できます。

# config/routes.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 認証済みコントローラ

Active Storageのすべてのコントローラは、デフォルトでpublicアクセスできます。生成されるURLではプレーンなsigned_idが使われ、推測は困難ですが、URLは永続的です。blobのURLを知っている人であれば、 ApplicationControllerbefore_actionでログインを必須にしていてもblobのURLにアクセス可能です。より高度なレベルの保護が必要な場合は、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
  # ApplicationController経由で
  # AuthenticateとSetCurrentAccountをincludeする

  def show
    redirect_to Current.account.logo.url
  end
end
<%= image_tag account_logo_path %>

このとき、一般からアクセス可能なURLでファイルにアクセスされるのを防ぐために、以下のようにActive Storageのデフォルトルーティングを無効にするとよいでしょう。

config.active_storage.draw_routes = false

6 ファイルをダウンロードする

アップロードしたblobに対して処理を行う必要がある場合(別フォーマットへの変換など)は、downloadを用いてblobのバイナリデータをメモリに読み込めます。

binary = user.avatar.download

場合によっては、blobをディスク上のファイルとしてダウンロードし、外部プログラム(ウイルススキャナーやメディアコンバーターなど)で処理できるようにしたいことがあります。添付ファイルのopenメソッドでblobをディスク上の一時ファイルにダウンロードできます。

message.video.open do |file|
  system '/path/to/virus/scanner', file.path
  # ...
end

重要なのは、このファイルはafter_createコールバックの時点ではアクセスできず、after_create_commitコールバックでのみアクセス可能になることです。

7 ファイルを解析する

Active Storageは、Active Jobにジョブをキューイングしてアップロードされるとファイルを解析します。解析されたファイルのメタデータハッシュには、analyzed: trueなどの追加情報が保存されます。analyzed?を呼び出すことで、blobが解析済みかどうかをチェックできます。

画像解析では、幅(width)と高さ(height)の属性が提供されます。

動画解析では、幅(width)と高さ(height)のほかに、再生時間(duration)、角度(angle)、アスペクト比( display_aspect_ratio)、動画の存在を表すvideo(boolean)と音声の存在を表すaudio(boolean)も提供されます。

音声解析では、再生時間(duration)とビットレート(bit_rate)の属性が提供されます。

8 画像、動画、PDFを表示する

Active Storageは、ファイルのさまざまな表示方法をサポートしています。

添付ファイルでrepresentationを呼び出すと、画像バリアントの表示や、動画やPDFのプレビュー表示が行えます。

representable?を呼び出せば、representationを呼び出す前に添付ファイルが表示可能かどうかをチェックできます。

ファイルフォーマットによっては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は、デフォルトで表示をlazyに処理します。

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

上のコードで生成される<img>タグには、ActiveStorage::Representations::RedirectControllerを指すsrc属性が追加されます。ブラウザがこのコントローラにリクエストを送信すると、リモートサービスへの302リダイレクトが返されます(プロキシモードの場合はファイルのコンテンツが返されます)。

ファイルが遅延読み込みされることで、単一用途URLのような機能を使っても最初のページ読み込みが遅くならなくなります。

遅延読み込みはほとんどのケースに適しています。

画像をただちに表示するURLを生成したい場合は、以下のように.processed.urlを呼び出せます。

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

Active Storageのバリアントトラッカーは、リクエストされた表示処理が以前行われていた場合にレコードをデータベースに保存することで、パフォーマンスを向上させます。つまり上のコードは、S3などのリモートサービスへのAPI呼び出しを1度だけ行い、バリアントが保存されると以後はそれを使います。バリアントトラッカーは自動的に実行されますが、config.active_storage.track_variants設定で無効にできます。

上のコード例を用いて1つのページ内で多数の画像をレンダリングすると、バリアントレコードの読み込みで「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 画像を変形する

画像を変形(transform)することで、画像を任意のサイズで表示できるようになります。

サイズ違いの画像(variant)を作成するには、添付ファイルでvariantを呼び出します。このメソッドには、バリアントプロセッサでサポートされている任意の変形処理を渡せます。

ブラウザがバリアントのURLにアクセスすると、Active Storageは元のblobを指定のフォーマットに遅延変形し、新しいサービスのある場所へリダイレクトします。

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

バリアントがリクエストされると、Active Storageは画像フォーマットに応じて自動的に変形処理を適用します。

  1. Content-Typeが可変(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から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 }) %>

10 ファイルのプレビュー

画像でないファイルの中にはプレビュー可能なものもあります(画像として表示されます)。たとえば、動画ファイルの最初のフレームを抽出してプレビューできます。Active Storageでは、動画とPDFドキュメントについては、すぐ使えるプレビュー機能をサポートしています。遅延生成されるプレビューへのリンクを作成するには、以下のように添付ファイルのpreviewメソッドを使います。

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

別のフォーマットのサポートを追加するには、独自のプレビューアを追加します。詳しくはActiveStorage::Previewドキュメントを参照してください。

11 ダイレクトアップロード

Active Storageは、付属のJavaScriptライブラリを用いて、クライアントからクラウドへのダイレクトアップロード(Direct Uploads)をサポートします。

11.1 利用法

  1. アプリケーションのJavaScriptバンドルにactivestorage.jsを追記します。

    アセットパイプラインを使う場合は以下のようにします。

    //= require activestorage
    

    npmパッケージを使う場合は以下のようにします。

    import * as ActiveStorage from "@rails/activestorage"
    ActiveStorage.start()
    
  2. file_fielddirect_upload: trueを追加します。

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

    FormBuilderを使っていない場合は、以下のようにdata属性を直接追加します。

    <input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
    
  3. サードパーティのストレージサービスにCORSを設定して、ダイレクトアップロードのリクエストを許可します。

  4. 以上で完了です。アップロードはフォーム送信時に開始されます。

11.2 CORS(Cross-Origin Resource Sharing)を設定する

サードパーティへのダイレクトアップロードを使えるようにするには、そのサービスで自分のアプリからのクロスオリジンリクエストを許可する必要があります。お使いのサービスの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が設定されている場合のみ必要)

Diskサービスはアプリのオリジンを共有するので、CORS設定は不要です。

11.2.1 設定例: S3のCORS
[
  {
    "AllowedHeaders": [
      "*"
    ],
    "AllowedMethods": [
      "PUT"
    ],
    "AllowedOrigins": [
      "https://www.example.com"
    ],
    "ExposeHeaders": [
      "Origin",
      "Content-Type",
      "Content-MD5",
      "Content-Disposition"
    ],
    "MaxAgeSeconds": 3600
  }
]
11.2.2 設定例: Google Cloud StorageのCORS
[
  {
    "origin": ["https://www.example.com"],
    "method": ["PUT"],
    "responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
    "maxAgeSeconds": 3600
  }
]
11.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>

11.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> なし すべてのダイレクトアップロードが終了した。

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

11.5 ライブラリやフレームワークとの統合

ダイレクトアップロード機能をJavaScriptフレームワークから利用したい場合や、ドラッグアンドドロップをカスタマイズしたい場合は、DirectUploadクラスを利用できます。選択したライブラリからファイルを1件受信したら、DirectUploadをインスタンス化してそのインスタンスのcreateメソッドを呼び出します。createには、アップロード完了時に呼び出すコールバックを1つ渡せます。

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 {
      // 適切な名前のhidden inputを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)
    }
  })
}

ファイルアップロードの進行状況をトラッキングする必要がある場合は、DirectUploadコンストラクタに第3パラメータを渡せます。DirectUploadはアップロード中にオブジェクトのdirectUploadWillStoreFileWithXHRメソッドを呼び出すので、以後はXHRで独自のプログレスハンドラをバインドできるようになります。

import { DirectUpload } from "@rails/activestorage"

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

  upload(file) {
    this.upload.create((error, blob) => {
      if (error) {
        // エラーハンドリングをここに書く
      } else {
      // 適切な名前のhidden inputをblob.signed_idの値とともにフォームに追加する
      }
    })
  }

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

  directUploadDidProgress(event) {
    // event.loadedとevent.totalでプログレスバーを更新する
  }
}

ダイレクトアップロードでは、ファイルがアップロードされたにもかかわらずレコードにまったくアタッチされないことがあります。アタッチされなかったアップロードを破棄するを参照してください。

12 テスト

結合テストやコントローラのテストでファイルのアップロードをテストするには、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

13 テスト中に作成したファイルを破棄する

13.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を使っている場合は、test環境で以下のようにインラインキューアダプタを使うように設定してください(purgeジョブが未来のいつかではなく、ただちに実行されるようにするため)。

# インラインジョブ処理を用いてpurgeをただちに行う
config.active_job.queue_adapter = :inline
13.1.1 結合テスト

システムテストの場合と同様、結合テスト(integration test)の場合もアップロードしたファイルの自動クリーンアップは行われません。アップロードしたファイルをクリーンアップしたい場合は、teardownコールバックで行えます。

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

並列テストDiskServiceを利用している場合は、Active Storage用の独自のフォルダをプロセスごとに設定する必要があります。これにより、teardownコールバックが呼ばれたときに、関連するプロセスのファイルだけが削除されるようになります。

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

13.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)に、filename:に対応するファイルを置きます。詳しくは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
13.2.1 フィクスチャをクリーンアップする

テストでアップロードされたファイルは各テストが終わるたびにクリーンアップされますが、フィクスチャファイルのクリーンアップはテスト完了時に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

14 その他のクラウドサービスのサポートを実装する

これら以外のクラウドサービスをサポートする必要がある場合は、サービスを実装する必要があります。 各サービスは、ファイルをアップロードしてクラウドにダウンロードするのに必要なメソッドを実装することで、ActiveStorage::Serviceを拡張します 。

15 アタッチされなかったアップロードを破棄する

アップロードされたファイルがレコードにまったくアタッチされないことがあります。これはダイレクトアップロードを使っている場合に発生する可能性があります。scope :unattachedを使うことでアタッチされなかったレコードをクエリで調べられます。以下はカスタム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で生成されるクエリは、大規模なデータベースを使うアプリケーションでは遅くなってユーザーの混乱を招く可能性があります。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加