【Rails】Shrineの使い方【プログラミング学習156日目】

# はじめに
プログラミングを始めて5ヶ月が経過しました。時が経つのは本当に早いですね。。。

先日1つ目のポートフォリオが完成し、2つ目のポートフォリオ制作に取り掛かっています。

画像をアップロード用のgemを調べていたところ、[Shrine](https://shrinerb.com/)というイケてそうなgemを見つけました。

1つ目ではRailsチュートリアルに従って、CarrierWaveを用いていたのですが、2作品目では違うものを扱ってみたい！という考えから、今回はこのShrineを用いてアップロード機能を実装することにしました。

# shrine gemとは？
- [Shrine (公式)](https://shrinerb.com/)
- [shrine (GitHub)](https://github.com/shrinerb/shrine)
- ファイル（主に画像）をアップロードするためのツール
- 特徴としては、CarrierWaveよりもシンプルであり、必要に応じてプラグインを追加することでリサイズやAWS S3へのダイレクトアップロードなどの機能を実装可能
- その他には、英語情報も含めて情報が多いことや、開発が止まってない（頻繁にcommitやプルリクエストされている）点が魅力的

# 開発環境
- Ruby 2.5.1
- Rails 5.2.2
- Devise 4.5.0
- Shrine 2.14.0

# 前提
- 認証系にはDeviseを用いており、`current_user`メソッドなどが使用できる状態であるとします。
- ユーザープロフィール画像をアップロードしたい、という状況を考えていきます。

# shrine gemの使い方
主に以下3つの兄弟記事を参考にShrineの導入を進めました。
- [Shrineを使って画像をアップロードする](https://re-engines.com/2017/04/21/shrine%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E7%94%BB%E5%83%8F%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B/)
- [Shrineでアップロードする際に画像を加工する
](https://re-engines.com/2017/05/05/shrine%E3%81%A7%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B/)- [ShrineでS3に画像をアップロードする](https://re-engines.com/2017/12/01/shrine-s3/)

今回は、下記3点について説明していきます。

1. Shrineの導入、`/public/uploads`フォルダへの格納機能の実装
2. 画像リサイズ機能の導入・バリデーション設定
3. AWS S3へのダイレクトアップロード機能の導入・設定

## 1. Shrineの導入
### gemの追加
まず、`shrine gem`を追加し、`$ bundle install`を実行します。

```ruby:Gemfile
gem 'shrine'
```

### 設定ファイルの作成
次に、`$ touch config/initializers/shrine.rb`にてShrineの設定ファイルを作成し、中身を記述していきます。

これで、画像データを送信した時に`/public/uploads/cache`フォルダ（一時保存用）および`/public/uploads/store`フォルダ中（永久保存用）に画像が入るようになります。

また、`cached_attachment_data`プラグインを追加することで、フォーム中で`= f.hidden_field :image, value: current_user.cached_image_data`のように、キャッシュデータを送信できるようになります。

```ruby:config/initializers/shrine.rb
require "shrine"
require "shrine/storage/file_system"

# アップロードするディレクトリの指定
Shrine.storages = {
    cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
    store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store")}

# 使用するプラグインの宣言
Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data
```

### Userモデルに`image_data`属性を追加
続いて、`$ rails g migration add_column_to_users image_data:text`でUserモデルに`image_data`属性を追加します。

ここで、`image_data`属性のデータ型は`text`である点に気をつけて下さい。
（`string`にするといざデータを送信した時に文字数制限に引っ掛かりエラーが出ました。）

その後、忘れずに`$ rails db:migrate`を実行します。

### ImageUploaderモデルの作成
`app/uploaders/image_uploader.rb`ファイルを作成し、中身を記述します。
基本部分の導入ではこれだけで問題ありません。

```ruby:app/uploaders/image_uploader.rb
class ImageUploader < Shrine
end
```

### Userモデルの編集
Userモデルに実装したアップローダについて追記します。
ここで、`[:image_data]`ではなく、`[:image]`とすることに注意です。

```ruby:app/models/user.rb
class User < ApplicationRecord
  include ImageUploader[:image]  # ここを追記、[:image_data]としないこと
  ・
  ・
  ・
end
```

### Strong Parameterの編集
Strong Parameterに`:image`を追加して、画像データが保存できるように設定します。
ここでも、`[:image_data]`ではなく、`[:image]`とすることに注意です。

また、今回はDeviseを使用しているので、`app/controllers/application_controller.rb`中のDevise用のStrong Parameter中に記述していますが、時と場合によってコントローラは変えて下さい。

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_in, keys: [:name, :email, :password, :password_confirmation])
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :email])
      # 今回は以下に追記、[:image_data]としないこと
    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :email, :password, :password_confirmation, :image])
  end
end
```

### ビューの実装
対象のビューを作成し、実装していきます。
今回はDeviseで自動生成されたユーザー編集ページ（`app/views/devise/registrations/edit.html.slim`）に記述しています。

`f.label`の部分だけ`image_data`となることに注意して下さい。

また、`f.hidden_field`にてキャッシュデータを送信するようにしています。
（`.cached_image_data`は前述の`cached_attachment_data`プラグインがなければエラーが出ます。）

```ruby:app/views/devise/registrations/edit.html.slim
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: {method: :put}) do |f|
  ・
  ・
  ・
  = f.label :image_data, "プロフィール画像"
  = f.hidden_field :image, value: current_user.cached_image_data
  = f.file_field :image
  ・
  ・
  ・
  = f.submit "ユーザー情報を更新"
```

以上で画像をアップロード機能は実装できました。
引き続き、画像のリサイズとAWS S3へのダイレクトアップロードについて説明していきます。

## 2. 画像リサイズ・バリデーション設定
画像のリサイズ・バリデーションについては、[shrine (GitHub)](https://github.com/shrinerb/shrine)中に全て書いているため、を見ながら進めるとスムーズです。

### ImageMagickのインストール

```:>_ターミナル
$ brew install imagemagick
```

### image_processing gemの追加
以下のgemを追加して`$ bundle install`を実行します。

```ruby:Gemfile
gem 'image_processing'
gem 'mini_magick'
```

### ImageUploaderモデルの編集
先程作成した`app/uploaders/image_uploader.rb`の中身を記述していきます。

```app/uploaders/image_uploader.rb
require "image_processing/mini_magick"

class ImageUploader < Shrine
  plugin :processing # allows hooking into promoting
  plugin :versions   # enable Shrine to handle a hash of files
  plugin :delete_raw # delete processed files after uploading

  process(:store) do |io, context|
    versions = { original: io } # retain original

    io.download do |original|
      pipeline = ImageProcessing::MiniMagick.source(original)

      versions[:large]  = pipeline.resize_to_limit!(800, 800)
      versions[:medium] = pipeline.resize_to_limit!(500, 500)
      versions[:small]  = pipeline.resize_to_limit!(300, 300)
    end

    versions # return the hash of processed files
  end
end
```

これによって、以下のようなハッシュが生成されるようになります。

そして、この設定をすることで**`photo.image[:medium].url`のような形でハッシュに格納された画像データをリサイズしてから取り出すことが可能になります。**

```ruby:保存される画像データの構造
photo.image_data #=>
# '{
#   "original": {"id":"9sd84.jpg", "storage":"store", "metadata":{...}},
#   "large": {"id":"lg043.jpg", "storage":"store", "metadata":{...}},
#   "medium": {"id":"kd9fk.jpg", "storage":"store", "metadata":{...}},
#   "small": {"id":"932fl.jpg", "storage":"store", "metadata":{...}}
# }'

photo.image #=>
# {
#   :original => #<Shrine::UploadedFile @data={"id"=>"9sd84.jpg", ...}>,
#   :large    => #<Shrine::UploadedFile @data={"id"=>"lg043.jpg", ...}>,
#   :medium   => #<Shrine::UploadedFile @data={"id"=>"kd9fk.jpg", ...}>,
#   :small    => #<Shrine::UploadedFile @data={"id"=>"932fl.jpg", ...}>,
# }

photo.image[:medium]           #=> #<Shrine::UploadedFile>
photo.image[:medium].url       #=> "/uploads/store/lg043.jpg"
photo.image[:medium].size      #=> 5825949
photo.image[:medium].mime_type #=> "image/jpeg"
```

### バリデーション
前述の`app/uploaders/image_uploader.rb`にバリデーションの項目を追記することでバリデーションが設定されます。
途中にある`plugin :validation_helpers`を入れ忘れないように注意して下さい。

```ruby:app/uploaders/image_uploader.rb
require 'image_processing/mini_magick'

class ImageUploader < Shrine
  plugin :processing
  plugin :versions
  plugin :delete_raw
  plugin :validation_helpers  # ここを追記

  process(:store) do |io, context|
    versions = {original: io}

    io.download do |original|
      pipeline = ImageProcessing::MiniMagick.source(original)

      versions[:large]  = pipeline.resize_to_limit!(800, 800)
      versions[:medium] = pipeline.resize_to_limit!(500, 500)
      versions[:small]  = pipeline.resize_to_limit!(300, 300)
    end

    versions
  end

  # 以下を追記
  Attacher.validate do
    validate_max_size 5 * 1024 * 1024, message: '5MBを超える画像はアップロードできません。'
    validate_mime_type_inclusion %w(image/jpeg image/png)
  end
end
```

以上で画像のリサイズとバリデーションの実装は完了です。

## 3. AWS S3へのダイレクトアップロード
### AWS S3バケットの作成
最初に、AWS側でバケット作成などをする必要がありますが、ここでは参考にしたURLだけ載せて割愛します。
(S3バケットの作成方法についての参考URL)
- [【AWS】S3のバケットを作ってみる](https://gootablog.com/aws-s3-create)

### CORSの設定
CORSについては以下の通りです。

> *CORSはCross-Origin Resource Sharingの略で「特定のドメインにロードされたクライアントウェブアプリケーションが異なるドメイン内のリソースと通信する方法を定義します」*
> *[ShrineでS3に画像をアップロードする](https://re-engines.com/2017/12/01/shrine-s3/)*

上記で作成したS3バケットにおいて、「アクセス制限」→「CORSの設定」→「CORS構成エディター」の順でクリックしていき下記の通り入力して「保存」をクリックします。

```
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://your.domain.com</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```

### Gemの追加
以下のgemを追加して$ bundle installを実行します。

```ruby:Gemfile
gem "aws-sdk-s3"
group :development, :test do
  gem 'dotenv-rails'
end
```

### `.env`にS3のアクセスキーなどの情報を記述
アクセスキー等は流出するとAWSを不正に悪用されてしまう危険があるので、晒さないようにします。

`$ touch .env`でアプリケーションのルートディレクトリに`.env`ファイルを追加してそのファイルに以下のように記述することで一安心です。

なお、この時に、`.gitignore`に`.env`を追加しておかないとプッシュした時にパスワード等が結局晒されることになるので注意が必要です。

```ruby:.env
S3_BUCKET = "..."
S3_REGION = "..."
S3_ACCESS_KEY_ID = "..."
S3_SECRET_ACCESS_KEY = "..."
```

### `shrine.rb`の編集
あとは設定ファイルをS3にダイレクトアップロード仕様に編集すれば完了です。

`require 'shrine/storage/s3'`という箇所と`if Rails.env.production?〜`の部分を追記していきます。

```ruby:config/initializers/shrine.rb
require "shrine"
require 'shrine/storage/file_system'
require 'shrine/storage/s3'  # ここを追記

# アップロードするディレクトリの指定
# ここを追記
if Rails.env.production?
  s3_options = {
    access_key_id:     ENV['S3_ACCESS_KEY_ID'],
    secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
    region:            ENV['S3_REGION'],
    bucket:            ENV['S3_BUCKET']}

  Shrine.storages = {
    cache: Shrine::Storage::S3.new(prefix: 'cache', **s3_options),
    store: Shrine::Storage::S3.new(prefix: 'store', **s3_options)}
else
  # この部分は元々記述していた
  Shrine.storages = {
      cache: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/cache'),
      store: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/store')}
end

# 使用するプラグインの宣言
Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data
```

### Railsサーバーの再起動
最後にRailsサーバーを再起動させてあげれば完了です！お疲れ様でした！

これでS3の方に画像データが保存されるようになっているはずです。

# まとめ
- Shrineの使い方をまとめた。
- ShrineはCarrierWaveよりもシンプルであり、必要に応じてプラグイン追加によって機能の拡張が可能である。
- Railsにて画像のリサイズやAWS S3へのダイレクトアップロードができるようにする設定を紹介した。

# 参考URL
- [Shrine (公式)](https://shrinerb.com/)
- [shrine (GitHub)](https://github.com/shrinerb/shrine)
- [shrine-rails-example (GitHub)](https://github.com/erikdahlstrand/shrine-rails-example)
- [Shrineを使って画像をアップロードする](https://re-engines.com/2017/04/21/shrine%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E7%94%BB%E5%83%8F%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B/)
- [Shrineでアップロードする際に画像を加工する
](https://re-engines.com/2017/05/05/shrine%E3%81%A7%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B/)
- [ShrineでS3に画像をアップロードする](https://re-engines.com/2017/12/01/shrine-s3/)
- [【AWS】S3のバケットを作ってみる](https://gootablog.com/aws-s3-create)