ActiveStorageを使ってお手軽にファイルアップロードを試す

最近ぼちぼちアウトプットしようとせこせこインプットしています。

前回Rails5.2でたからActionCable試すという舐めた記事を書いたので今回はちゃんとActiveStorageを試します！

## ActiveStorageとは

ActiveStorageはRails5.2から標準で搭載されるようになったファイルアップローダーです。  
AWS S3などのクラウドストレージへのアップロードも設定で手軽に実装できる代物です。

https://github.com/rails/rails/tree/master/activestorage

今日はこのActiveStorageを使って簡単な画像も投稿できる機能を作ります。  

## 導入

まずは適当に`rails new`でサンプルアプリプロジェクトをつくります。  
(DBにpostgresql使いますがなんでもOKです)

```sh
$ rails new active_storage_sample -d postgresql
```

プロジェクトができたらGemfile内の`mini_magick`のコメントアウトを外します。  
画像処理のために導入するためです。

```ruby:Gemfile
# Use ActiveStorage variant
gem 'mini_magick', '~> 4.8' # ここのコメントアウト外す
```

コメントアウトを外したら`bundle install`でインストールしておきます。  
これで導入に必要なものは整いました。

## ActiveStorageをインストールする

ActiveStorageを使うためにはまずインストールのコマンドを実行します。  

```
$ rails active_storage:install
```

実行するとActiveStorageでアップロードファイルを管理するテーブルのmigrationファイルが生成されるので`rails db:migrate`を実行しておきましょう。  

生成されるテーブルは以下の二つになります。

### active_storage_blobs

|カラム名|保存内容|
|-----|-----|
|id|ID|
|key|ファイルを一意に識別するkey|
|filename|アップロードしたファイルの名前|
|content_type|ファイルの種類|
|metadata|メタデータ(画像なら縦横の大きさなどが格納される)|
|byte_size|ファイルサイズ(byte単位)|
|checksum|チェックサム|
|created_at|作成日時|

### active_storage_attachments

|カラム名|保存内容|
|-----|-----|
|id|ID|
|name|モデルの属性名(Userモデルのavatarとか)|
|record_type|モデル名(Userとか)|
|record_id|record_typeのモデルのID|
|blob_id|active_storage_blogsのID|
|created_at|作成日時|

## ActiveStorageの設定を確認する

ファイルの格納先は`config/storage.yml`で設定されています。

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

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

例えば`local`の設定を使えばプロジェクトルートにある`storageディレクトリ`配下にファイルを格納するという意味になります。

次に`config/environments/development.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
```

という記載があり格納先に`config/storage.yml`で定義した`local`の設定を使うよという設定が書いてあります。

これで設定の確認は完了です。  

## ファイルアップロードする画面を作る

まずは画像アップロードできるようにUserモデルを作ります。  

```
$ rails g model user name:string
```

ActiveStorageを扱う際は画像アップロード用のカラムを定義する必要はありません。  
コマンドを実行したら`rails db:migrate`を実行します。  

Userモデルができたら、ActiveStorageを使うための記述を追記しておきます。  

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar # 追記
end
```

この追記を行うことでUserモデルにavatarという画像を扱う属性が付与されます。  
has_oneがあればhas_manyもあります。  
`has_many_attached`を利用すれば複数画像を扱うこともできます。  

詳しくはこちらで

https://railsguides.jp/active_storage_overview.html#has-many-attached

ActionStorageを使うための記載はだいたい終了なので、あとはざっと画面などを作成します。  

```ruby:config/routes.rb 
Rails.application.routes.draw do
  root "users#index"
  resources :users, only: [:new, :create]
end
```

```ruby:app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      redirect_to root_url, notice: 'Add User'
    else
      render :new
    end
  end

  private

  def user_params
    params.fetch(:user, {}).permit(:name, :avatar)
  end
end
```

```erb:app/views/users/new.html.erb
<h1>Add User</h1>

<%= form_with model: @user, local: true do |form| %>
  <div>
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div>
    <%= form.label :avatar %>
    <%= form.file_field :avatar %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>
```

```erb:app/views/users/index.html.erb
<h1>Users Index</h1>
<%= link_to 'Add user', new_user_path %>

<ul>
  <% @users.each do |user| %>
    <li>
      <p><%= user.name %></p>
      <%= image_tag user.avatar %>
    </li>
  <% end %>
</ul>
```

ここまでやると、こんな感じになります。

* 投稿画面
![20180418_02.png](https://qiita-image-store.s3.amazonaws.com/0/134832/9fb76369-6473-fb84-06c0-31d16ff3a6fb.png)


* 一覧画面
![20180418_03.png](https://qiita-image-store.s3.amazonaws.com/0/134832/87599001-b6a9-f390-f291-bf94f684915c.png)


という感じですごく簡単に画像アップロードが実装できてしまいます！

## S3にアップロードできるようにする

### S3でバケットを作る
せっかくなのでS3にアップロードできるようにしてみます。  

まずはAWS S3で適当な名前のバケットを作成します。  
(わかりやすくactive-storage-sampleというバケットを作りました)

![20180418_01.png](https://qiita-image-store.s3.amazonaws.com/0/134832/07f8ede5-d90f-1e69-5379-a09f0c22b2ad.png)


作成したらS3を操作できるIAMユーザーを作成してアクセスキーとシークレットキーを取得しておきましょう。  

### 格納先の設定をS3に向くように変更する

`config/storage.yml`の以下のコメントアウトを外します。

```yml: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: active-storage-sample
```

`config/environments/development.rb`で先ほど`local`となっていることを確認した設定を`amazon`に変更します。

```ruby:config/environments/development.rb
config.active_storage.service = :amazon # :localを:amazonに変更
```

またS3へのアップロードを行うために`aws-sdk-s3`を導入します。  
Gemfileに以下を追記しましょう。

```ruby:Gemfile
gem 'aws-sdk-s3'
```

追記したら`bundle install`しておきます。

### アプリケーションからS3につなぎこむ

アプリケーションからS3につなぎこむためにIAMユーザーを作成して取得したアクセスキーとシークレットキーを設定しましょう。  

Rails5.2からcredentialsという機密情報を暗号化する機能が追加されています。  
これを使ってアクセスキーとシークレットキーを設定します。  

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

これを実行するとエディタ(この記載だとvim)が開いてキーを設定できます。  

エディタが開いたら以下のように設定を記載します。

```yml
aws:
  access_key_id: アクセスキー
  secret_access_key: シークレットキー
```

記載したら保存して閉じます。  
すると`config/credentials.yml.enc`というファイルと`config/master.key`というファイルができています。  
`config/credentials.yml.enc`は暗号化されているので内容を見ることはできません。  

暗号化のキーには`config/master.key`に記載されているキーが使用されています。(環境変数`RAILS_MASTER_KEY`が設定されていれば、これを使う)

設定を確認したい場合は`rails credentials:show`を実行すれば生のデータを確認することができます。  

先ほど`config/storage.yml`でコメント外したところに

```ruby
Rails.application.credentials.dig(:aws, :access_key_id)
```

という記載がありましたが、これはcredentialsのawsのaccess_key_idを取得するという記載になります。

ここまで設定できれば画像を保存した際にローカルではなくS3にファイルが格納されるようになっています。  
クラウドストレージへのアップロードも少ない手順で実現できてしまうのはお手軽で素晴らしいですね。

#### ちなみに

`config/master.key`や`RAILS_MASTER_KEY`の取り扱い間違えて消してしまったりすると、credentialsの複合ができなくなり詰んでしまうので注意しましょう。  
詰んでしまった場合は`config/credentials.yml.enc`を削除して`rails credentials:edit`で1から設定し直すと持ち直せる可能性があります。(チーム開発だとツラみがすごいかも…)

## 最後に

ActiveStorage手軽で便利なのですが、バリデーションやキャッシュの機能を持ち合わせていないとどこかでみたのでアップロード時にファイルチェックが必要な要件があったりすると扱いにくいかもしれません。  

ただ厳密さが求められないような状況であれば素早く実装できるので検討してみても良さそうです！