このガイドはActive Recordモデルにファイルを添付する方法について説明します。
本ガイドの内容:
Active Storageは、Amazon S3、Google Cloud Storage、Microsoft Azure Storageなどのクラウドストレージサービスへのファイルのアップロードや、ファイルをActive Recordオブジェクトにアタッチする機能を提供します。 development環境とtest環境向けのローカルディスクベースのサービスを利用できるようになっており、ファイルを下位のサービスにミラーリングしてバックアップや移行に用いることも可能です。
Active Storageは、アプリケーションにアップロードした画像の変形や、PDFや動画などの画像以外のアップロードファイルの内容を代表する画像の生成、任意のファイルからのメタデータ抽出にも利用できます
Active Storageの多くの機能は、Railsによってインストールされないサードパーティソフトウェアに依存しているため、別途インストールが必要です。
画像分析や画像加工のためにimage_processing
gemも必要です。Gemfile
のimage_processing
gemをコメント解除するか、必要に応じて追加します。
gem "image_processing", ">= 1.2"
ImageMagickは、libvipsに比べて知名度が高く普及も進んでいます。しかしlibvipsは10倍高速かつメモリ消費も1/10です。JPEGファイルの場合、libjpeg-dev
をlibjpeg-turbo-dev
に置き換えると2〜7倍高速になります。
サードパーティのソフトウェアをインストールして使う前に、そのソフトウェアのライセンスを読んで理解しておきましょう。特にMuPDFはAGPLでライセンスされており、利用目的によっては商用ライセンスが必要です。
Active Storageは、アプリケーションのデータベースで active_storage_blobs
、active_storage_variant_records
、active_storage_attachments
という名前の3つのテーブルを使います。
新規アプリケーション作成した後(または既存のアプリケーションをRails 5.2にアップグレードした後)に、rails active_storage:install
を実行して、これらのテーブルを作成するマイグレーションファイルを作成します。マイグレーションファイルを実行するにはrails db:migrate
を使います。
active_storage_attachments
は、使うモデルのクラス名を保存するポリモーフィックjoinテーブルです。モデルのクラス名を変更した場合は、このテーブルに対してマイグレーションを実行して背後のrecord_type
をモデルの新しいクラス名に更新する必要があります。
モデルの主キーに整数値ではなくUUIDを使っている場合は、生成されるマイグレーションファイルのactive_storage_attachments.record_id
とactive_storage_variant_records.id
のカラム型も変更する必要があります。
Active Storageのサービスはconfig/storage.yml
で宣言します。アプリケーションが使うサービスごとに、名前と必要な構成を指定します。
次の例では、local
、test
、amazon
という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
組み込みのサービスアダプタ(Disk
やS3
など)およびそれらに必要な設定について、詳しくは後述します。
環境固有の設定ファイルが優先されます。たとえば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 %>
Diskサービスはconfig/storage.yml
で宣言します。
local: service: Disk root: <%= Rails.root.join("storage") %>
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クライアント設定によってコネクションが数分間保持されてしまい、リクエストの待ち行列が発生する可能性があります。
Gemfile
にaws-sdk-s3
gemを追加します。
gem "aws-sdk-s3", require: false
Active Storageのコア機能では、s3:ListBucket
、s3:PutObject
、s3:GetObject
、s3:DeleteObject
という4つのパーミッションが必要です。パブリックアクセスの場合はs3:PutObjectAcl
も必要です。ACLの設定といったアップロードオプションを追加で設定した場合は、この他にもパーミッションが必要になることがあります。
環境変数、標準SDKの設定ファイル、プロファイル、IAMインスタンスのプロファイルやタスクロールを使いたい場合は、上述のaccess_key_id
、secret_access_key
、region
を省略できます。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ドキュメントを参照してください。
Azure Storageサービスはconfig/storage.yml
で宣言します。
azure: service: AzureStorage storage_account_name: "" storage_access_key: "" container: ""
Gemfile
にazure-storage-blob
gemを追加します。
gem "azure-storage-blob", "~> 2.0", require: false
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"
Gemfile
にgoogle-cloud-storage
gemを追加します。
gem "google-cloud-storage", "~> 1.11", require: false
ミラーサービスを定義すると、複数のサービスを同期できます。ミラーサービスは、複数の下位サービスにアップロードや削除をレプリケーションします。
ミラーサービスは、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
すべてのセカンダリサービスがアップロードを受信しますが、ダウンロードは常にプライマリサービスで行われます。
ミラーサービスはダイレクトアップロードと互換性があります。新しいファイルはプライマリサービスに直接アップロードされます。ダイレクトアップロードされたファイルをレコードにアタッチすると、セカンダリサービスにコピーするバックグラウンドジョブがキューに登録されます。
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 S3、Google Cloud Storage、Microsoft Azureのドキュメントをそれぞれ参照してください。Amazon S3ではs3:PutObjectAcl
パーミッションも必要です。
既存のアプリケーションをpublic: true
に変更する場合は、バケット内のあらゆるファイルが一般公開されて読み取り可能になっていることを確認してから切り替えてください。
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) %>
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
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_type
にidentify: 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
が設定されます。
添付ファイルをモデルから削除するには、添付ファイルに対してpurge
を呼び出します。
Active Jobを使うようにアプリケーションが設定されている場合は、バックグラウンドで削除を実行できます。purgeすると、blobとファイルがストレージサービスから削除されます。
# avatarと実際のリソースファイルを同期的に破棄します。 user.avatar.purge # Active Jobを介して、関連付けられているモデルと実際のリソースファイルを非同期で破棄します。 user.avatar.purge_later
Active Storageは「リダイレクト」と「プロキシ」という2種類のファイル配信をサポートしています。
Active Storageのすべてのコントローラは、デフォルトでpublicアクセスできます。生成されるURLは推測が困難ですが、設計上は永続的なURLになります。ファイルをより高度なレベルで保護する必要がある場合は、認証済みコントローラの実装を検討してください。
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_path
やrails_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)
ファイルをプロキシ(proxy)することもオプションで可能です。この場合、リクエストのレスポンスで、アプリケーションサーバーがファイルデータをストレージサービスからダウンロードします。プロキシモードは、CDN上のファイルを配信する場合に便利です。
以下のように、Active Storageがデフォルトでプロキシを利用するように設定できます。
# config/initializers/active_storage.rb Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy
特定の添付ファイルを明示的にプロキシしたい場合は、rails_storage_proxy_path
やrails_storage_proxy_url
という形式のURLヘルパーを利用できます。
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
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])) %>
Active Storageのすべてのコントローラは、デフォルトでpublicアクセスできます。生成されるURLではプレーンなsigned_id
が使われ、推測は困難ですが、URLは永続的です。blobのURLを知っている人であれば、 ApplicationController
のbefore_action
でログインを必須にしていてもblobのURLにアクセス可能です。より高度なレベルの保護が必要な場合は、ActiveStorage::Blobs::RedirectController
、ActiveStorage::Blobs::ProxyController
、ActiveStorage::Representations::RedirectController
、ActiveStorage::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
アップロードした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
コールバックでのみアクセス可能になることです。
Active Storageは、Active Jobにジョブをキューイングしてアップロードされるとファイルを解析します。解析されたファイルのメタデータハッシュには、analyzed: true
などの追加情報が保存されます。analyzed?
を呼び出すことで、blobが解析済みかどうかをチェックできます。
画像解析では、幅(width
)と高さ(height
)の属性が提供されます。
動画解析では、幅(width
)と高さ(height
)のほかに、再生時間(duration
)、角度(angle
)、アスペクト比( display_aspect_ratio
)、動画の存在を表すvideo
(boolean)と音声の存在を表すaudio
(boolean)も提供されます。
音声解析では、再生時間(duration
)とビットレート(bit_rate
)の属性が提供されます。
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
メソッドを呼び出します。これらのメソッドは直接呼ぶことも可能です。
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
画像を変形(transform)することで、画像を任意のサイズで表示できるようになります。
サイズ違いの画像(variant)を作成するには、添付ファイルでvariant
を呼び出します。このメソッドには、バリアントプロセッサでサポートされている任意の変形処理を渡せます。
ブラウザがバリアントのURLにアクセスすると、Active Storageは元のblobを指定のフォーマットに遅延変形し、新しいサービスのある場所へリダイレクトします。
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
バリアントがリクエストされると、Active Storageは画像フォーマットに応じて自動的に変形処理を適用します。
Content-Typeが可変(config.active_storage.variable_content_types
の設定に基づく)で、Web画像を考慮しない場合(config.active_storage.web_image_content_types
)の設定に基づく)は、PNGに変換される。
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 }) %>
画像でないファイルの中にはプレビュー可能なものもあります(画像として表示されます)。たとえば、動画ファイルの最初のフレームを抽出してプレビューできます。Active Storageでは、動画とPDFドキュメントについては、すぐ使えるプレビュー機能をサポートしています。遅延生成されるプレビューへのリンクを作成するには、以下のように添付ファイルのpreview
メソッドを使います。
<%= image_tag message.video.preview(resize_to_limit: [100, 100]) %>
別のフォーマットのサポートを追加するには、独自のプレビューアを追加します。詳しくはActiveStorage::Preview
ドキュメントを参照してください。
Active Storageは、付属のJavaScriptライブラリを用いて、クライアントからクラウドへのダイレクトアップロード(Direct Uploads)をサポートします。
アプリケーションのJavaScriptバンドルにactivestorage.js
を追記します。
アセットパイプラインを使う場合は以下のようにします。
//= require activestorage
npmパッケージを使う場合は以下のようにします。
import * as ActiveStorage from "@rails/activestorage" ActiveStorage.start()
file_field
にdirect_upload: true
を追加します。
<%= form.file_field :attachments, multiple: true, direct_upload: true %>
FormBuilder
を使っていない場合は、以下のようにdata属性を直接追加します。
<input type="file" data-direct-upload-url="<%= rails_direct_uploads_url %>" />
サードパーティのストレージサービスに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
が設定されている場合のみ必要)Diskサービスはアプリのオリジンを共有するので、CORS設定は不要です。
[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "PUT" ], "AllowedOrigins": [ "https://www.example.com" ], "ExposeHeaders": [ "Origin", "Content-Type", "Content-MD5", "Content-Disposition" ], "MaxAgeSeconds": 3600 } ]
[ { "origin": ["https://www.example.com"], "method": ["PUT"], "responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"], "maxAgeSeconds": 3600 } ]
<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>
イベント名 | イベントの対象 | イベントデータ(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> |
なし | すべてのダイレクトアップロードが終了した。 |
上記のイベントを用いて、アップロードの進行状況をプログレスバー表示できます。
以下は、アップロードされたファイルをフォームに表示するコードです。
// 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; }
ダイレクトアップロード機能を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でプログレスバーを更新する } }
ダイレクトアップロードでは、ファイルがアップロードされたにもかかわらずレコードにまったくアタッチされないことがあります。アタッチされなかったアップロードを破棄するを参照してください。
結合テストやコントローラのテストでファイルのアップロードをテストするには、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
システムテストでは、トランザクションをロールバックすることでテストデータをクリーンアップしますが、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
システムテストの場合と同様、結合テスト(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
既存のフィクスチャに添付ファイルを追加できます。最初に、独立したストレージサービスを作成します。
# 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
テストでアップロードされたファイルは各テストが終わるたびにクリーンアップされますが、フィクスチャファイルのクリーンアップはテスト完了時に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
これら以外のクラウドサービスをサポートする必要がある場合は、サービスを実装する必要があります。
各サービスは、ファイルをアップロードしてクラウドにダウンロードするのに必要なメソッドを実装することで、ActiveStorage::Service
を拡張します 。
アップロードされたファイルがレコードにまったくアタッチされないことがあります。これはダイレクトアップロードを使っている場合に発生する可能性があります。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ガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。