【Rails 5.2】 Active Storageの使い方

Active Storageについて調べると、「軽く触ってみた」ノリの記事ばかりで、特に複数のファイルを扱う方法などがなかなか見つからなかったので、使い方のまとめ記事を作りました。:muscle:


# Active Storage とは

Active Storageは、Rails5.2で追加された、**ファイルアップロード**を行うための機能です。これを使えば、フォームで画像の投稿機能などが簡単に作れます。また、Amazon S3, Google Cloud Storage, Microsoft Azure Storageなどの**クラウドストレージサービス**に対するファイルのアップロードを簡単に行うことができます。クラウドストレージの他に、ローカルディスクにファイルを保存することもできます。
**Paperclip, Carrierwave, Dragonflyなどのgemの代わりになる**とされていますが、現時点ではバリデーションとキャッシュ関連の機能は残念ながら備えていません。使うには**Rails 5.2以上**にアップグレードする必要があります。


# Active Storage を使えるようにする

```sh
$ rails active_storage:install
$ rails db:migrate
```

ここは気になる方だけ読んでいただければ大丈夫なのですが、このマイグレーションによって``active_storage_blobs``と``active_storage_attachments``という名前のテーブルが2つ生成されます。これらのテーブルは``Blob``と``Attachment``の2つのモデルが使います。``Blob``はファイル名、ファイルの種類、バイト数、誤り検出符号などのメタデータを保持するモデルで、``Attachment``モデルは、BlobオブジェクトとActive Recordオブジェクトを紐付けるための中間テーブルです。なお、Active Storageを使う際、直接``Blob``と``Attachment``モデルに触れる必要はありません。(時間返せ)


# Active Record モデルを用意する

コメントに1枚、もしくは、複数枚の画像を添付できるようにしたいとしましょう。まずは``Comment``モデルを用意する必要があります。``generate resource``コマンドで基本的なルートと、モデル、空のコントローラを生成しましょう。

```sh
$ rails g resource comment content:text
$ rails db:migrate
```
ここで、画像用のカラムを用意する必要がない点もActive Storageの特徴の一つです。


# 1つの添付ファイルの場合

``Comment``モデルに1つの画像を添付するには、``has_one_attached``を使います。

```ruby
class Comment < ApplicationRecord
  has_one_attached :image
end
```
``:image``はファイルの呼び名で、``:photo``、``:avatar``、``:hoge``など、ファイルの用途に合わせて好きなものを指定してください。ここで、Imageモデルなどを作る必要はないです。Active Storageは裏側で``Blob``と``Attachment``モデルを使って、こそこそと``comment.image``を使えるようにしてくれます。(有能すぎ)

では、コントローラとビューの中身を書きましょう。

```ruby:comments_controller.rb
class CommentsController < ApplicationController
  def new
    @comment = Comment.new
  end

  def create
    @comment = Comment.create params.require(:comment).permit(:content, :image) # POINT
    redirect_to @comment
  end

  def show
    @comment = Comment.find(params[:id])
  end

  def edit
    @comment = Comment.find(params[:id]) 
  end

  def update
    @comment = Comment.find(params[:id])
    @comment.update params.require(:comment).permit(:content, :image) # POINT
    redirect_to @comment
  end
end
```
```erb:new.html.erb
<%= form_with model: @comment, local: true  do |form| %>
  <%= form.text_area :content %><br>
  <%= form.file_field :image %><br>
  <%= form.submit %>
<% end %>
```
```erb:show.html.erb
<% if @comment.image.attached? %>
  <%= image_tag @comment.image %>
<% end %>
```

ほとんど典型的なコードですね。ポイントは``create``と``update``アクションのところで、このようにして、``image``があたかも``Comment``のカラムであるかのように扱うことで、フォームの``file_field``で選択された画像を``Comment``オブジェクトと紐付けます。これも、Active Storageの特徴の一つです。

``create``と``update``アクションを使えない場合は、``@comment.image.attach(params[:comment][:image])``で画像を後から``Comment``オブジェクトと紐付けることもできます。

例えば、画像の選択を任意にしたい場合は以下のようなコードになります。

```ruby
@comment = Comment.create params.require(:comment).permit(:content)
if image = params[:comment][:image]
  @comment.image.attach(image)
end
```

画像を表示するには``image_tag``に`` @comment.image``を渡すだけです。

結果はこのような感じになります。
![ezgif-2-b17e951b9f.gif](https://qiita-image-store.s3.amazonaws.com/0/225997/c7204de5-f954-d0cf-df7d-11a92c185c02.gif)

# 複数の添付ファイルの場合
複数の添付ファイルを使いたい場合は、上記のコードを少しだけ変更します。

- ``has_one_attached``の代わりに``has_many_attached``を使う
- ``comment.image``の代わりに``comment.images``を使う
- ``file_field``に``multiple: true``を追記して、複数ファイルの選択を許可

```ruby:comment.rb
class Comment < ApplicationRecord
  has_many_attached :images
end
```
```ruby:comments_controller.rb
class CommentsController < ApplicationController
  def new
    @comment = Comment.new
  end

  def create
    @comment = Comment.create params.require(:comment).permit(:content, images: [])
    redirect_to @comment    
  end

  def show
    @comment = Comment.find(params[:id])
  end

  def edit
    @comment = Comment.find(params[:id]) 
  end

  def update
    @comment = Comment.find(params[:id])
    @comment.update params.require(:comment).permit(:content, images: [])
    redirect_to @comment
  end
end
```
```erb:new.html.erb
<%= form_with model: @comment, local: true  do |form| %>
  <%= form.text_area :content %><br><br>
  <%= form.file_field :images, multiple: true %><br>
  <%= form.submit %>
<% end %>
```
```erb:show.html.erb
<% if @comment.images.attached? %>
  <% @comment.images.each do |image| %>
    <%= image_tag image %> <br>
  <% end %>
<% end %>
```

一枚の時同様、``@comment.images.attach(params[:comment][:images])``も使えます。

結果はこちら
![ezgif-2-e15b099da1.gif](https://qiita-image-store.s3.amazonaws.com/0/225997/6687ccdc-10e7-33f1-944e-aa9a7f0647ba.gif)



# ファイルの保存先の変更

ファイルの保存先は、各環境の設定ファイルに記載します。
まずは、 ``config/environments/development.rb`` と ``production.rb`` の中身を覗いてみましょう。

```ruby:config/environments/development.rb
  # ...

  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :local

  # ...
```
```ruby:config/environments/production.rb
  # ...

  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :local

  # ...
```


初期状態では、開発環境(development)、本番環境(production)ともに保存先は ``:local`` に設定されています。
この ``local`` とは、 ``config/storage.yml`` で定義された保存先の名前です。
これを変更するには、``:local`` のところを ``:amazon``, ``:google``, ``:microsoft`` のいづれかと置き換え、``config/storage.yml``の方に、必要な認証情報などの値を入力します。

今度は、``config/storage.yml``の中身を覗いてみましょう。

```yaml:config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

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

# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket

# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
#   service: AzureStorage
#   storage_account_name: your_account_name
#   storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
#   container: your_container_name
```

先ほど見た保存先の ``local`` は、使用するサービスが ``Disk`` (ローカルディスク)に設定れていて、railsアプリ直下の``/storage``ディレクトリがファイルの保存先に指定されています。



あと、親切に、コメントがたくさんついてますね。この中から適切なところのコメントを解除することで、好きなストレージサービスを使うことができます。

また、お使いのサービスのgemをGemfileに追記する必要があります。これは、``aws-sdk-s3``, ``google-cloud-storage``, ``azure-storage``のいづれかになります。

---

なお、アクセスキーは、セキュリティ上、**Rails Credentials**を使って入力するのが好ましいです。
これは、**APIキーなどの機密情報を暗号化して保存するための機能**で、実はこれもRails 5.2で追加された新機能です。これまでの``secrets.yml``の代わりになります。
``<%= Rails.application.credentials.dig(...) %>``の部分は、まさにCredentialsに入力されたデータを読み込んでいますのでコメントをそのまま使いましょう。

- Credentialsにデータを入力・編集するには ``$ rails credentials:edit`` と叩きます。<br>エラーが出た場合は、``$ EDITOR=vim rails credentials:edit``で、お使いのエディターを指定してください。例: ``vim``, ``emacs``, ``atom``, ``code``, ``subl``
- 入力した内容は``config/master.key``を用いて暗号化され、``config/credentials.yml.enc``が生成されます。
- 復号された中身は ``$ rails credentials:show`` で確認できます。


## Amazon S3 を使う場合
S3は基本的に、ファイルをアップロードし、そのURLを取得できる、AWSのストレージサービスです。

### 🔰 Amazon S3でバケットを用意する方法
バケットは画像などのファイルをアップロードできる入れ物です。
AWSコンソール → サービス → ストレージ → S3　→ 「バケットを作成する」

|項目|入力・選択|
|:--|:--|
|パケット名|例: my-rails-app-bucket|
|リージョン|例: ap-northeast-1 (東京)|
|パブリックアクセス許可を管理する|このバケットに読み取りアクセス権限をする|
|上記以外|全部デフォルトのまま|

### 🔰 Amazon S3でアクセスキーを作る方法
AWSコンソール → サービス → セキュリティ → IAM → 「ユーザー」 → 「ユーザーを追加」

|項目|入力・選択|
|:--|:--|
|ユーザー名|例: s3user|
|アクセスの種類|プログラムによるアクセス|

「既存のポリシーを直接アタッチ」 → 「S3」で検索 → 「AmazonS3FullAccess」を選択 → 「次へ」 → 「ユーザーの作成」
表示された「アクセスキー ID」と「シークレットアクセスキー」 をメモ帳などにコピペして保管する。（一度しか表示されません）


### 保存先をAmazon S3に指定する

まず最初に必要なAWS S3のGemをインストールします。

```ruby:Gemfile
gem "aws-sdk-s3", require: false
```
Gemfile変更後、``$ bundle install``と叩きます。

次に、保存先をAmazon S3に指定します。

```ruby:config/environments/production.rb
# 本番環境(production)の保存先を:localから:amazonに変更

config.active_storage.service = :amazon
```
```ruby:config/environments/development.rb
# 開発環境(development)でAmazon S3の動作を確認したい場合はこちらの方も変更しましょう

config.active_storage.service = :amazon
```
```yaml:config/storage.yml
# 以下の部分をコメント解除する
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  # 以下の２行を変更
  region: ap-northeast-1 #東京の場合
  bucket: my-rails-app-bucket #自分で作成したS3のバケットの名前
```

前にも述べましたが、``<%= Rails.application.credentials.dig(...) %>``の部分は、**Credentials**の情報を読み込んでいます。早速、CredentialsにAmazon S3へのアクセスキーを入力しましょう。

```sh
$ EDITOR=vim rails credentials:edit
```

上の例ではVimが開きます。Atomをお使いの場合、``EDITOR=atom``としてください。
VSCodeなら、``EDITOR=code``、SublimeTextなら``EDITOR=subl``。

```yml
aws:
 access_key_id: 123 #ここに自分のアクセスキーIDをコピペ
 secret_access_key: 456 #ここに自分のシークレットアクセスキーをコピペ
```
:bulb: Vim: ``i``で入力開始、``esc`` → ``ZZ``で終了

入力した内容は``$ rails credentials:show``で確認できます。

以上で保存先をAmazon S3に変更できました。:tada:


# :warning: 重要
**Credentials**をお使いの場合は、``config/credentials.yml.enc``の中身を復号化するために必要な``config/master.key``ファイルを本番環境にも配置する必要があります。しかし、``master.key``はセキュリティ上、Gitで管理してはいけないため、標準で .gitignore されてあり、PaaSなどのGitリポジトリをそのままデプロイする本番環境(Herokuなど)の場合は、``master.key``　の中身をコピーして、環境変数 ``RAILS_MASTER_KEY`` として用意する必要があります。

ちなみに、Herokuをお使いの場合は ``$ heroku config:set RAILS_MASTER_KEY=123``で環境変数を用意できます。

# 参考
https://www.engineyard.com/blog/active-storage
https://qiita.com/yatmsu/items/08b95e837ac7f24fb443
https://afreshcup.com/home/2017/07/06/introduction-to-active-storage-part-3-google-cloud-storage