Active Storage の概要

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

このガイドを読むと下記の内容が理解できるでしょう。

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_blobsactive_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で宣言します。アプリケーションが使うサービスごとに、名前と必要な構成を指定します。 次の例では、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: ""

利用するサービスを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

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

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

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

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

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

環境変数、標準SDKの設定ファイル、プロファイル、IAMインスタンスのプロファイルやタスクロールを使いたい場合は、上述のaccess_key_idsecret_access_keyregionを省略できます。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を呼び出します。

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

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

Current.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 画像を変換する

画像のバリエーションを作成するには、Blobvariantを呼び出します。このメソッドには、画像プロセッサでサポートされる任意の変換方法を渡せます。デフォルトの画像プロセッサは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 ダイレクトアップロードのインストール

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

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

    //= require activestorage
    

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

    import * as ActiveStorage from "activestorage"
    ActiveStorage.start()
    
  2. ファイル入力に以下を記述してダイレクトアップロードのURLを指定します。

     <%= form.file_field :attachments, multiple: true, direct_upload: true %>
    
    
  3. 以上で完了です。アップロードはフォーム送信時に開始されます。

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

以下は、アップロードされたファイルをフォームに表示するコードです。

// 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 inputblob.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ガイドは下記のサポーターから継続的な支援を受けています。