はじめまして!スタートアップでサーバーサイドエンジニアをやっています。なかのです!
TechTrain Advent Calendar 2019(User ver)の15日目を担当します!
今回はタイトルの通り、Rails 6 ActiveStorageを使用してS3に画像ファイルをアップロードし、取得した画像をリサイズして表示する方法をお話ししたいと思います。
はじめに
まず、簡単にActiveStorageについて説明しますと
Active StorageとはAmazon S3、Google Cloud Storage、Microsoft Azure Storageなどの クラウドストレージサービスへのファイルのアップロードや、ファイルをActive Recordオブジェクトにアタッチする機能を提供します。development環境とtest環境向けのローカルディスクベースのサービスを利用できるようになっており、ファイルを下位のサービスにミラーリングしてバックアップや移行に用いることもできます。
アプリケーションでActive Storageを用いることで、ImageMagickで画像のアップロードを変換したり、 PDFやビデオなどの非画像アップロードの画像表現を生成したり、任意のファイルからメタデータを抽出したりできます。
(https://railsguides.jp/active_storage_overview.html 参照)
開発環境は以下になります。
Ruby '2.6.5'
Rails '6.0.2'
準備
今回はUserに紐づく画像ファイルをアップロードしたいと思いますので、scaffoldを用いてさくっと必要な部分を作ります!
$ rails new activerecord-sample
$ cd activerecord-sample
$ rails generate scaffold user name:string
$ rails db:migrate
$ rails s
rails s
でサーバーを立ち上げ、localhost:3000/users
にアクセスし、下の画像のような画面が表示されたらセットアップ完了です。
S3にバケットを作成する
Amazon Simple Storage Service(Amazon S3)は、インターネット用のストレージサービスで、データ (写真、動画、ドキュメントなど)を保存しておくために利用されます。使用するためには事前にバケットを作成しておく必要があります。
S3のセットアップは以下の記事を参考にしてみてください。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/user-guide/create-configure-bucket.html
今回は以下のような設定でバケットを作成しました。
項目 | 入力・選択 |
---|---|
バケット名 | activestorage-sample-bucket |
リージョン | ap-northeast-1 (東京) |
パブリックアクセス許可を管理する | このバケットに読み取り・書き込みアクセス権限をする |
上記以外 | 全部デフォルトのまま |
S3でアクセスキーを作る。
アクセスキーを作る方法は、こちらを参考にしてみてください。
https://tech-blog.s-yoshiki.com/2019/06/1292/
IAMユーザー作成後に表示された「アクセスキー ID」と「シークレットアクセスキー」 は後ほど使用しますので誰にも教えないように保管しておいてください。
ActiveStorageの導入
ここから実際にActiveStorageを導入していきたいと思います。
以下のコマンド実行してActiveStorageをinstallしてください。
$ rails active_storage:install
$ rails db:migrate
active_storage_attachments
とactive_storage_blobs
というテーブルが作成されていれば、インストール成功です。
ActiveStorageは、初期設定ではDisk内(storage以下
)にアップロードしたファイルデータを保存するようになっているため、amazon s3のストレージを使用する記述を追加します。
まずは使用するストレージの設定(今回だとamazon s3)を以下のファイルに追加してください。
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
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: <%= Rails.application.credentials.dig(:aws, :s3, :region) %>
bucket: <%= Rails.application.credentials.dig(:aws, :s3, :bucket) %>
次に利用するサービスをActiveStorageに認識させます。
#ファイルをAmazon S3に保存する
config.active_storage.service = :amazon
S3のためのgemが必要なので、Gemfile
に以下の記述を追加してbundle installしてください。
gem "aws-sdk-s3", require: false
$ bundle install
最後にS3の環境設定を追加します。
Rails6 から各環境でcredentialsの管理が出来るようになったので、以下のコマンドを入力し、development環境でのcredentialsファイルを作成して、S3の設定を追加してください。
$ ./bin/rails credentials:edit --environment development
こちらのコマンドを入力した際、もしcredentialsが作成されていなかったら新たにconfig/credentials
以下にdevelopment.yml.enc
とdevelopment.key
というファイルを作成してくれます。
以下のように編集してください。
aws:
access_key_id: #先ほど取得したaccess_key_idをいれてください。
secret_access_key: #先ほど取得したsecret_access_keyをいれてください。
s3:
region: ap-northeast-1
bucket: activestorage-sample-bucket
```
編集が完了したら、値が取得出来ているか確認してみましょう。
$ rails c
irb(main):001:0> Rails.application.credentials.dig(:aws, :access_key_id)
=> "設定したaccess_key_id"
irb(main):002:0> Rails.application.credentials.dig(:aws, :secret_access_key)
=> "設定したsecret_access_key"
irb(main):003:0> Rails.application.credentials.dig(:aws, :s3, :region)
=> "ap-northeast-1"
irb(main):004:0> Rails.application.credentials.dig(:aws, :s3, :bucket)
=> "activestorage-sample-bucket"
値がちゃんと取れていれば、設定完了です。
## ActiveRecordの実装
UserとAttachmentとBlobの関係を以下の図に表します。
![スクリーンショット 2019-12-15 17.16.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227345/72357f15-fddd-276f-4f81-35a0d30094c8.png)
### Userモデルに1つのファイルを紐づける場合
1ユーザーに1つの画像ファイルしか紐づかない場合、上記の図のNが1になります。まずはこのパターンを実装していきたいと思います。
```app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar
end
```
Userモデルにavatarという属性を追加しました。今回はavatarという命名にしましたが、用途に合わせて自由に指定することが出来ます。
ActiveStorageでは、ファイルデータを保存するためにそれぞれのテーブルに個別にカラムを用意しなくても上記の記述を追加するだけで`active_storage_attachments`と`active_storage_blobs`が裏側でよしなに処理してくれます。また、今回は1つのファイルを添付するため`has_one_attached`という記述をしています。
画像を投稿出来るようにフォームを追加します。
`app/views/users/_form.html.erb`のブロック内に以下の記述を追加してください。
```app/views/users/_form.html.erb
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
# 以下の部分を追加
<div class="field">
<%= form.file_field :avatar %>
</div>
```
ストロングパラメーターもavatarを許可するようにします。
```app/controllers/users_controller.rb
def user_params
params.require(:user).permit(:name, :avatar)
end
```
最後にユーザー詳細画面で画像が表示されるようにしましょう。
```app/views/users/show.html.erb
<p>
<strong>Name:</strong>
<%= @user.name %>
# 以下を追記
<%= image_tag url_for(@user.avatar) %>
</p>
```
では、早速`rails s`をして、投稿してみましょう!
![てす.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227345/1e4ed530-8bda-c556-7920-2b8e2d0edf69.gif)
### Userモデルに複数のファイルを紐づける場合
次は複数投稿投稿出来るようにしてみますz。
```app/models/user.rb
class User < ApplicationRecord
has_many_attached :images
end
```
今度はUserモデルにimagesという属性を追加しました。
userに複数の画像を紐づけるために`has_many_attached`を使用しています。
画像を複数投稿出来るようにフォームを追加します。
`app/views/users/_form.html.erb`のブロック内に以下の記述を追加してください。
```app/views/users/_form.html.erb
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
# 以下の部分を追加
<div class="field">
<%= form.file_field :images, multiple: true %>
</div>
```
`multiple: true`にすることで画像を一度に複数選択出来るようになります。
ストロングパラメーターもimagesを許可するようにします。
```app/controllers/users_controller.rb
def user_params
params.require(:user).permit(:name, images: [])
end
```
最後に、ユーザー詳細画面で画像が表示されるようにします。
```app/views/users/show.html.erb
<p>
<strong>Name:</strong>
<%= @user.name %>
# 以下を追記
<% @user.images.each do |image| %>
<%= image_tag url_for(image) %>
<% end %>
</p>
```
これで複数投稿が出来るようになったので、`rails s`をして試してみましょう!
![てす2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227345/44cba93c-ae89-62d9-a850-9bb88c2366bc.gif)
## image_processingを用いたリサイズ
画像投稿した後は、画面サイズに合わせて画像を取得したくなりますよね。Rails6から`image_processing`というgemを使用することが推奨されているため、そちらを使用してリサイズを行っていきます。
`Gemfile`に`image_processing`がコメントアウトされていると思いますので、アンコメントしてbundle install します。
```Gemfile
gem 'image_processing', '~> 1.2'
```
また、ImageMagickをinstallする必要がありますので、Mac OSXを使用している方は`brew install imagemagick`でinstallしてください。
これで設定は完了です。
リサイズして表示するように設定していきます。variantメソッドを用いることで簡単にリサイズを行うことが出来ます。先ほど、複数投稿時に使用したviewに追加して、表示を見てみます。
```app/views/users/show.html.erb
<% @user.images.each do |image| %>
<%= image_tag image.variant(resize_to_limit: [100, 100]) %>
<% end %>
```
![スクリーンショット 2019-12-15 21.08.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227345/9a1c6479-a850-dd37-3129-c8c0c43bb4e1.png)
簡単にリサイズされましたね。
## おまけ
ActiveStorageでよく使用するメソッドやValidationについて、軽く書いておきたいと思います。
まず、よく使用するメソッド
`attached?`メソッド
添付ファイルを持っているかどうかを調べます。
irb(main):001:0> user = User.last
=> #<User id: 20, name: "なかの", created_at: "2019-12-15 11:24:56", updated_at: "2019-12-15 11:24:56">
irb(main):002:0> user.images.attached?
=> true
irb(main):007:0> user2 = User.new
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):008:0> user2.images.attached?
=> false
`purge`メソッド
Attach、Blob、S3から添付ファイルを削除します。
irb(main):009:0> user = User.last
=> #<User id: 20, name: "なかの", created_at: "2019-12-15 11:24:56", updated_at: "2019-12-15 11:24:56">
irb(main):010:0> user.images.attached?
=> true
irb(main):011:0> user.images.purge
=> #<ActiveRecord::Associations::CollectionProxy []>
irb(main):012:0> user.images.attached?
=> false
こちらのメソッドはRollbackが起きた際にS3とデータの不整合が起こりやすいため、使用時は注意しなくてはいけません。ActiveRecordのトランザクションとActiveStorageについての話は、こちらの記事がとてもわかりやすいのでおすすめです。
[https://tech.smarthr.jp/entry/2018/09/14/130139](https://tech.smarthr.jp/entry/2018/09/14/130139)
`detach`メソッド
Attachから添付ファイルのレコードを削除します。
irb(main):001:0> user = User.last
=> #<User id: 21, name: "なかの", created_at: "2019-12-15 12:26:06", updated_at: "2019-12-15 12:26:06">
irb(main):002:0> user.images.attached?
=> true
irb(main):003:0> user.images.detach
irb(main):004:0> user.images.attached?
=> false
ActiveStorageでは専用のValidationが用意されていないため、各自で作成しなくてはいけません。
簡単にContent_Typeを確認するValidationを作ってみました。
```app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar
validate :validate_avatar
def validate_avatar
errors.add(:avatar, "画像データではありません。") unless image?
end
def image?
return '' unless avatar.attached?
%w[image/jpg image/jpeg image/png image/gif].include?(avatar.blob.content_type)
end
end
```
## おわりに
ActiveStorageを使ってみて、思ったより簡単に導入できるので、単純な画像投稿機能などにはオススメだと思いました。
ただ、デフォルトで署名付きURLを取得してしまうため、CDNなどを使用する際は少し工夫が必要になるかなと思いました。
次回書けたら書きます・・・。