1 Active Storageについて
Active StorageとはAmazon S3、Google Cloud Storage、Microsoft Azure Storageなどの クラウドストレージサービスへのファイルのアップロードや、ファイルをActive Recordオブジェクトにアタッチする機能を提供します。development環境とtest環境向けのローカルディスクベースのサービスを利用できるようになっており、ファイルを下位のサービスにミラーリングしてバックアップや移行に用いることもできます。
アプリケーションでActive Storageを用いることで、ImageMagickで画像のアップロードを変換したり、 PDFやビデオなどの非画像アップロードの画像表現を生成したり、任意のファイルからメタデータを抽出したりできます。
2 セットアップ
Active Storageは、アプリケーションのデータベースで active_storage_blobs
とactive_storage_attachments
という名前の2つのテーブルを使用します。
新規アプリケーション作成した後(または既存のアプリケーションをRails 5.2にアップグレードした後)に、rails active_storage:install
を実行して、これらのテーブルを作成するmigrationファイルを作成します。
migrationファイルを実行するにはrails db:migrate
をお使いください。
active_storage_attachments
は、使うモデルのクラス名を保存するポリモーフィックjoinテーブルです。モデルのクラス名を変更した場合は、このテーブルに対してマイグレーションを実行して背後のrecord_type
をモデルの新しいクラス名に更新する必要があります。
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: ""
利用するサービスを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
内蔵されているサービスアダプタ(Disk
やS3
など)およびそれらに必要な設定について、詳しくは後述します。
2.1 Diskサービス
Diskサービスはconfig/storage.yml
で宣言します。
local: service: Disk root: <%= Rails.root.join("storage") %>
2.2 Amazon S3サービス
S3サービスはconfig/storage.yml
で宣言します。
amazon: service: S3 access_key_id: "" secret_access_key: "" region: "" bucket: ""
Gemfile
にaws-sdk-s3
gemを追加します。
gem "aws-sdk-s3", require: false
Active Storageのコア機能では、s3:ListBucket
、s3:PutObject
、s3:GetObject
、s3:DeleteObject
という4つのパーミッションが必要です。ACLの設定といったアップロードオプションを追加で設定した場合は、この他にもパーミッションが必要になることがあります。
環境変数、標準SDKの設定ファイル、プロファイル、IAMインスタンスのプロファイルやタスクロールを使いたい場合は、上述のaccess_key_id
、secret_access_key
、region
を省略できます。Amazon S3サービスでは、AWS SDK documentationに記載されている認証オプションをすべてサポートします。
2.3 Microsoft Azure Storageサービス
Azure Storageサービスはconfig/storage.yml
で宣言します。
azure: service: AzureStorage storage_account_name: "" storage_access_key: "" container: ""
Gemfile
にMicrosoft Azure Storageクライアントのgemを追加します。
gem "azure-storage", 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) %> 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: ""
Gemfile
にGoogle Cloud Storageクライアントのgemを追加します。
gem "google-cloud-storage", "~> 1.8", 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
ファイルはprimaryサービスから提供されます。
この機能はダイレクトアップロード機能との互換性がありません。
3 ファイルをレコードに添付する
3.1 has_one_attached
has_one_attached
マクロは、レコードとファイルの間に1対1のマッピングを設定します。各レコードには1つのファイルを添付できます。
たとえば、アプリケーションにUser
モデルがあるとします。各userにavatarを持たせたい場合は、以下のようにUser
モデルを定義します。
class User < ApplicationRecord has_one_attached :avatar end
以下のように書くことでavatar付きの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を添付するにはavatar.attach
を呼び出します。
user.avatar.attach(params[:avatar])
avatar.attached?
で特定のuserがavatarを持っているかどうかを調べられます。
user.avatar.attached?
3.2 has_many_attached
has_many_attached
マクロは、レコードとファイルの間に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?
3.3 File/IO Objectsをアタッチする
HTTPリクエスト経由では配信されないファイルをアタッチする必要が生じる場合があります。たとえば、ディスク上で生成したファイルやユーザーが送信したURLからダウンロードしたファイルをアタッチしたい場合や、モデルのテストでfixtureファイルをアタッチしたい場合などが考えられます。これを行うには、オープンIOオブジェクトとファイル名を1つ以上含むハッシュを渡します。
@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf')
可能であれば、content_type:
も指定しましょう。Active Storageは、渡されたデータからファイルのcontent_typeの判定を試みますが、判定できない場合はcontent_typeにフォールバックします。
@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')
content_type:
を指定せず、Active Storageがファイルのcontent_typeを自動的に判別できない場合は、デフォルトでapplication/octet-stream
が設定されます。
4 ファイルを削除する
添付ファイルをモデルから削除するには、添付ファイルに対して purge
を呼び出します。
Active Jobを使用するようにアプリケーションが設定されている場合は、バックグラウンドで削除を実行できます。消去すると、BLOBとファイルがストレージサービスから削除されます。
# avatarと実際のリソースファイルを同期的に破棄します。 user.avatar.purge # Active Jobを介して、関連付けられているモデルと実際のリソースファイルを非同期で破棄します。 user.avatar.purge_later
5 ファイルにリンクする
アプリケーションを指すblobのパーマネントURLを生成します。アクセス時には、実際のサービスエンドポイントへのリダイレクトが返されます。 このインダイレクションによってパブリックURLを実際のURLと切り離し、たとえば、高可用性のために添付ファイルを別サービスにミラーリングすることもできます。リダイレクトのHTTPの有効期限は5分です。
url_for(user.avatar)
ダウンロードリンクを作成するには、rails_blob_ {path | url}
ヘルパーを使います。このヘルパーでdisposition:
を設定できます。
rails_blob_path(user.avatar, disposition: "attachment")
コントローラやビューのコンテキストの外(バックグラウンドジョブやcronジョブなど)からリンクを作成したい場合、rails_blob_path
を用いて以下のようにアクセスできます。
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
6 ファイルをダウンロードする
アップロードしたblobに対して処理を行う(別フォーマットへの変換など)必要が生じることがあります。ActiveStorage::Blob#download
を用いてblobのバイナリデータをメモリに読み込めます。
binary = user.avatar.download
場合によっては、blobをディスク上のファイルとしてダウンロードし、外部プログラム(ウイルススキャナーやメディアコンバーターなど)で処理できるようにしたいことがあります。ActiveStorage::Blob#open
でblobをディスク上のtempfileにダウンロードできます。
message.video.open do |file| system '/path/to/virus/scanner', file.path # ... end
7 画像を変換する
画像のバリエーションを作成するには、Blob
でvariant
を呼び出します。このメソッドには、画像プロセッサでサポートされる任意の変換方法を渡せます。デフォルトの画像プロセッサはMiniMagickですが、Vipsも使えます。
バリアントを有効にするには、image_processing
gemをGemfile
に追加します。
gem 'image_processing', '~> 1.2'
ブラウザがバリアントURLにヒットすると、Active Storageは元のblobを指定のフォーマットに遅延変換し、新しいサービスのロケーションにリダイレクトします。
<%= image_tag user.avatar.variant(resize: "100x100") %>
画像プロセッサをVipsに切り替えるには、config/application.rb
に以下を追加します。
# 別の画像プロセッサとしてVipsを使う config.active_storage.variant_processor = :vips
8 ファイルのプレビュー
画像でないファイルの中にはプレビューできるものもあります(画像として表示されます)。たとえば、動画ファイルの最初のフレームを抽出してプレビューできます。Active Storageでは、動画とPDFドキュメントについてすぐ使えるプレビュー機能をサポートしています。
<ul> <% @message.files.each do |file| %> <li> <%= image_tag file.preview(resize: "100x100>") %> </li> <% end %> </ul>
プレビュー画像の抽出にはサードパーティのアプリケーションが必要です(動画の場合はffmpeg
、PDFの場合はmutool
)。これらのライブラリはRailsでは提供されていません。組み込みのプレビューソフトウェアを使う場合は、自分でインストールしなければなりません。サードパーティのソフトウェアをインストールして使う場合、そのソフトウェアがライセンスにどのように影響をするかを理解しておいてください。
9 ダイレクトアップロード
Active Storageは、付属のJavaScriptライブラリを用いて、クライアントからクラウドへのダイレクトアップロードをサポートします。
9.1 ダイレクトアップロードのインストール
-
アプリケーションのJavaScriptバンドルに
activestorage.js
を追記します。アセットパイプラインを使う場合は以下のようにします。
//= require activestorage
npmパッケージを使う場合は以下のようにします。
import * as ActiveStorage from "activestorage" ActiveStorage.start()
-
ファイル入力に以下を記述してダイレクトアップロードのURLを指定します。
<%= form.file_field :attachments, multiple: true, direct_upload: true %>
以上で完了です。アップロードはフォーム送信時に開始されます。
9.2 ダイレクトアップロードのJavascriptイベント
イベント名 | イベントの対象 | イベントデータ(event.detail ) |
説明 |
---|---|---|---|
direct-uploads:start |
<form> |
None | ダイレクトアップロードフィールドのファイルを含むフォームが送信された。 |
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> |
None | すべてのダイレクトアップロードが終了した。 |
9.3 例
上記のイベントを用いて、アップロードの進行状況をプログレスバー表示できます。
以下は、アップロードされたファイルをフォームに表示するコードです。
// 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">${file.name}</span> </div> `) }) 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.4 ライブラリやフレームワークとの統合
ダイレクトアップロード機能を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) { 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) { // Use event.loaded and event.total to update the progress bar } }
10 システムテスト中に保存したファイルを破棄する
システムテストでは、トランザクションをロールバックすることでテストデータをクリーンアップしますが、destroy
はオブジェクトに対して呼び出されないため、添付ファイルはそのままでは決してクリーンアップされません。
添付ファイルを破棄したい場合は、after_teardown
コールバックで行えます。このコールバックを実行すると、テスト中に作成されたすべての接続を確実に完了するので、Active Storageでファイルが見つからないというエラーは表示されなくなります。
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] def remove_uploaded_files FileUtils.rm_rf("#{Rails.root}/storage_test") end def after_teardown super remove_uploaded_files end end
システムテストで添付ファイルを含むモデルの削除を検証し、かつActive Jobを使っている場合は、test環境でインラインキューアダプタを使うよう設定します。これにより、purgeジョブが(未来の不確定の時刻ではなく)ただちに実行するようになります。
また、test環境向けに別のサービス定義を使えば、開発中に作成したファイルがテスト中に削除されないようにできます。
# インラインジョブ処理でただちにジョブを実行する config.active_job.queue_adapter = :inline # test環境では別のファイルストレージを使う config.active_storage.service = :local_test
11 結合テスト中に保存したファイルを破棄する
システムテストの場合と同様、結合テスト(integration test)の場合もアップロードしたファイルの自動クリーンアップは行われません。アップロードしたファイルをクリーンアップしたい場合は、after_teardown
コールバックで行えます。このコールバックを実行すると、テスト中に作成されたすべての接続を確実に完了するので、Active Storageでファイルが見つからないというエラーは表示されなくなります。
module RemoveUploadedFiles def after_teardown super remove_uploaded_files end private def remove_uploaded_files FileUtils.rm_rf(Rails.root.join('tmp', 'storage')) end end module ActionDispatch class IntegrationTest prepend RemoveUploadedFiles end end
12 その他のクラウドサービスのサポートを実装する
これら以外のクラウドサービスをサポートする必要がある場合は、サービスを実装する必要があります。 各サービスは、ファイルをアップロードしてクラウドにダウンロードするのに必要なメソッドを実装することで、ActiveStorage::Serviceを拡張します 。
支援・協賛
Railsガイドは下記のサポーターから継続的な支援を受けています。