Active Storage + S3 による添付ファイル機能を試してみる

# はじめに

この記事では Rails 5.2 の目玉機能である [Active Storage](https://github.com/rails/rails/tree/master/activestorage) を試してみます。具体的にはベータ版の Rails 5.2 を使って、添付ファイルを AWS S3 にアップロードしてみます。

なお JavaScript を使って直接ファイルを Web クライアントから直接アップロードできる [Direct uploads](https://github.com/rails/rails/tree/master/activestorage#direct-uploads) という機能については触れませんので悪しからず。

# 準備

Rails のバージョン `5.2.0.beta2` を使って `rails new` します。

```rb:Gemfile
gem 'rails', '5.2.0.beta2'
```

追加で次の Gem をインストールします。

```rb:Gemfile
gem 'mini_magick', '4.8.0'
gem 'aws-sdk', '3.0.1'
```

次のコマンドを実行します。

```shell-session
$ bin/rails active_storage:install
```

マイグレーションを実行すると `active_storage_blobs` (メタデータ用) と `active_storage_attachments` (ポリモーフィック関連用) という 2 つのテーブルが作成されます。

```shell-session
$ bin/rails db:migrate
```

AWS アクセスキーを設定します。

```shell-session
$ EDITOR=vim bin/rails credentials:edit
```

```yaml:vim
aws:
  access_key_id: REPLACE_WITH_ACCESS_KEY_ID
  secret_access_key: REPLACE_WITH_SECRET_ACCESS_KEY

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: # 略
```

その後 `config/storage.yml` で `amazon` の設定を入力します。

```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: hidamari
```

最後に Active Storage が AWS S3 を使うように設定を更新します。

```config/environments/development.rb
config.active_storage.service = :amazon # development 環境の初期値は :local になっている。
```

# 動作検証

次の User モデルを使って動作を検証します。なおデータベースには PostgreSQL を使いました。

## モデル : 添付ファイル = 1 : 1 の場合

```app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar
end
```

まだ画像が添付されていない状態です。

```rb
yuno = User.find_by(name: 'ゆの')
yuno.avatar.attached? #=> false
```

ではファイルをアップロードしてみます。

```rb
filepath = File.join(Dir.home, 'Downloads/yuno.jpg')
yuno.avatar.attach(io: File.open(filepath), filename: File.basename(filepath), content_type: 'image/jpg')
# Enqueued ActiveStorage::AnalyzeJob (Job ID: ca6acf25-2971-4fea-a4a7-7c1103b5ebf5) to Async(default) with arguments: #<GlobalID:0x00007fc30dc37bc0 @uri=#<URI::GID gid://activestorage-sandbox/ActiveStorage::Blob/1>>
#=> #<ActiveStorage::Attachment id: 1, name: "avatar", record_type: "User", record_id: 1, blob_id: 1, created_at: "2017-12-04 06:03:44">
```

`attach` メソッドを実行すると `ActiveStorage::AnalyzeJob` というバックグラウンドジョブが登録されます。登録後してすぐにジョブが実行されました。

```
Performing ActiveStorage::PurgeJob (Job ID: ca6acf25-2971-4fea-a4a7-7c1103b5ebf5) from Async(default) with arguments: #<GlobalID:0x00007fc30dc37bc0 @uri=#<URI::GID gid://activestorage-sandbox/ActiveStorage::Blob/1>>
# 略
Performed ActiveStorage::PurgeJob (Job ID: ca6acf25-2971-4fea-a4a7-7c1103b5ebf5) from Async(default) in 152.65ms
# 略
Performed ActiveStorage::AnalyzeJob (Job ID: 36223552-3459-4325-943a-28de6d1d45f0) from Async(default) in 322.1ms
```

アップロード後に [url_for](http://api.rubyonrails.org/v5.1/classes/ActionDispatch/Routing/UrlFor.html) を使うと期限付き URL (デフォルトでは 5 分) を発行できます。

```rb
yuno.avatar.attached? #=> true

# Rails console で url_for を使う。
# 設定ファイルで default_url_options を設定済であれば app.url_for でも動くかもしれない。
include(Rails.application.routes.url_helpers)
default_url_options[:host] = 'localhost:3000'

url_for(yuno.avatar)
#=> "http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBEUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--726ebe29a44d2ee4214513f49e4a9c55b4af76f5/yuno.jpg"
```

Rails server を起動してこの URL にアクセスすると、S3 上の画像ファイルをダウンロードするための期限付き URL にリダイレクトされます。

## モデル : 添付ファイル = 1 : 多 の場合

```app/models/user.rb
class User < ApplicationRecord
  has_many_attached :files
end
```

ひとつのモデルに複数のファイルを添付することも可能です。

```rb
yuno = User.find_by(name: 'ゆの')
yuno.files.attached? #=> false

filepath = File.join(Dir.home, 'Downloads/yuno.pdf')
yuno.files.attach(io: File.open(filepath), filename: File.basename(filepath), content_type: 'application/pdf')

filepath = File.join(Dir.home, 'Downloads/yuno.mp4')
yuno.files.attach(io: File.open(filepath), filename: File.basename(filepath), content_type: 'video/mp4')

yuno.files.attached? #=> true
yuno.files.count #=> 2
```

ちなみに動画ファイルや PDF ファイルの場合、プレビュー画像 (サムネイル用？) を生成することも可能なようです。README.md には説明がまだ書かれていませんが、単体テストを読んでいて知りました。

```rb
video_file = yuno.files.last
preview = video_file.blob.preview(resize: '640x280').processed # 実行時に ffmpeg のログが出力された。

url_for(preview.image)
#=> "http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBFUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--262878ebb566bf1aa31ad7a8355147a25422cd37/yuno.png"
```

# 感想

従来は添付ファイルのアップロードに [Paperclip](https://github.com/thoughtbot/paperclip) を使っていましたが、同様のことを外部の Gem なしに実現できるのはすごく楽ですね。正式版が待ち遠しいです :innocent: