[Rails6.0.2]CarrierWaveを基本に忠実に使ってみる

Rails

最近、CarrierWaveでものすごくハマったので、仕様確認がてら触ってみることにした。ハマった内容は、CarrierWave側のバグかもしれない。それはともかく、基本的なところから試していく。

準備

RailsにCarrierWaveをインストールする

Gemfileにgemを追記する。

# Gemfile

gem 'carrierwave', '~> 2.0'

ターミナルからインストールする

$ bundle install

Uploaderを作成する

アップローダーを作成する。このアップローダーはいろんな設定ができるもの。用途に応じて利用するのが一般的。運用中のアプリケーションの場合は、すでにあるだろう。

$ rails g uploader Image
Running via Spring preloader in process 10393
      create  app/uploaders/image_uploader.rb

画像を管理するカラムの追加

今回は1からアプリを作っているので、scaffoldでUserモデルを作る。profile_imageで画像を管理する。

$ rails g scaffold user name:string profile_image:string

すでにモデルがある場合は、通常のmigrationでstringでカラムを作る

$ rails g migration add_profile_image_to_users profile_image:string

追加した場合は、users_controllerにストロングパラメータを設定するの忘れないように。

マイグレートする

$ rails db:migrate

Uploaderをマウントする

# /app/models/user.rb

class User < ApplicationRecord
  mount_uploader :profile_image, ImageUploader
end

View編

入力フォームの編集

# /app/views/users/_form.html.erb

<div class="field">
  <%= form.label :profile_image %>
  <%= form.file_field :profile_image %>
</div>

画像が表示されない

画像を表示する

下記のように変更する

# /app/views/users/index.html.erb

# scaffoldの場合は、<td><%= user.profile_image %></td>となっているところを下記に変更する
<td><%= image_tag(user.profile_image_url, width: 100) if user.profile_image? %></td>
画像の表示ができた

Edit画面で表示させたい

index.html.erbと同様の記述をする

# /app/views/users/_form.html.erb

<div class="field">
  <%= image_tag(@user.profile_image_url, width: 100) if @user.profile_image? %>
  <%= form.label :profile_image %>
  <%= form.file_field :profile_image %>
</div>

表示ができた。このままだと、ファイルを選択したあとバリデーションで再度editページを表示したとき、ファイル選択が解除されてしまう。

UserモデルでNameを入力必須のバリデーションで設定し、下記のように実行する。なお、保存されている背景がグレーっぽい写真はmy-image.jpg。新たに利用している背景が緑色の写真はdemo-image.jpg。

nameを空, demo-image.jpgを選択して、Update Userをクリック
バリデーションエラーが発生。選択した画像に変更されているように見える。nameを入力してUpdate Userをクリック
Updateは成功したが、画像がmy-image.jpgのままで変更されていない。

バリデーションなどの時に再度画像を選択しなくても保存できるようにする。

hiddenに :[カラム名]_cache を渡す

# /app/views/users/_form.html.erb

<div class="field">
  <%= image_tag(@user.profile_image_url, width: 100) if @user.profile_image? %>
  <%= form.label :profile_image %>
  <%= form.file_field :profile_image %>
  <%= form.hidden_field :profile_image_cache %>
</div>

ストロングパラメータで :[カラム名]_cache を許可する

# /app/controllers/users_controller.rb

def user_params
  params.require(:user).permit(:name, :profile_image, :profile_image_cache)
end

実行結果は下記のようになる。

同様にdemo-image.jpg、nameを空でUpdate Userをクリック
先ほどと同様に選択したファイルの画像に変更されている.。この状態でnameを入力してUpdate Userをクリック

demo-image.jpgがアップロードされている

コントローラー等で、profile_image.cache! のように知人に言われたし、見かけるが、このぐらい単純な処理だと不要。hiddenにprofile_image_cacheを渡すのと、ストロングパラメータで設定してあげれば大丈夫。

画像を削除したい

・viewに削除用のチェックボックス(パラメータ名 :[カラム名]_image)を用意
・ストロングパラメーターに:[カラム名]_imageを追加する

# /app/views/users/_form.html.erb

  <div class="field">
    <%= image_tag(@user.profile_image_url, width: 100) if @user.profile_image? %>
    <%= form.label :profile_image %>
    <%= form.file_field :profile_image %>
    <%= form.hidden_field :profile_image_cache %>
    <label>
      <%= form.check_box :remove_profile_image %>
      画像を削除する
    </label>
  </div>
# /app/controllers/users_controller.rb

def user_params
  params.require(:user).permit(:name, :profile_image, :profile_image_cache, :remove_profile_image)
end

実際に動作を試してみる。

チェックを入れてUpdate Userをクリックする
Profile Imageが空になっている。実際に物理ファイルもサーバーから削除されている。

ちなみに、デフォルトだとファイルを選択した状態で削除にチェックを入れて、Updateをしても削除が優先されるため、同様の結果になる。

設定編

ファイルの形式を設定したい

画像を想定しているところでは画像だけ、文書だけを想定しているところでは文書だけをアップロードさせるようにしたいという要望がある。

今回は画像のケース。

ジェネレーターでアップローダーを作成した場合は、下記の内容のコメントアウトがあるので外すだけ。例えば、CsvUploaderのようなものを作った場合は、 %w(csv tsv) などを指定してあげる。

# /app/uploaders/image_uploader.rb
 
  def extension_whitelist
    %w(jpg jpeg gif png)
  end

実行結果

テキストファイルをアップロードしてUpdateをしてみる

txtファイルは許可されていなくて、jpg, jpeg, gif, pingにしろ促してくれている

エラーメッセージが英語。。。

バリデーションなどのCarrierWaveが出力するエラーの日本語化

localeファイルを追加もしくはlocaleファイルに追記する。

localeファイルを新しく追加した場合は、サーバーを再起動しないと反映されないので注意。

# /config/locales/ja.yml

ja:
  errors:
    messages:
      carrierwave_processing_error: 処理できませんでした
      carrierwave_integrity_error: は許可されていないファイルタイプです
      carrierwave_download_error: はダウンロードできません
      extension_whitelist_error: "%{extension}ファイルのアップロードは許可されていません。アップロードできるファイルタイプ: %{allowed_types}"
      extension_blacklist_error: "%{extension}ファイルのアップロードは許可されていません。アップロードできないファイルタイプ: %{prohibited_types}"
      content_type_whitelist_error: "%{content_type}ファイルのアップロードは許可されていません。アップロードできるファイルタイプ: %{allowed_types}"
      content_type_blacklist_error: "%{content_type}ファイルのアップロードは許可されていません"
      rmagick_processing_error: "rmagickがファイルを処理できませんでした。画像を確認してください。エラーメッセージ: %{e}"
      mini_magick_processing_error: "MiniMagickがファイルを処理できませんでした。画像を確認してください。エラーメッセージ: %{e}"
      min_size_error: "を%{min_size}以上のサイズにしてください"
      max_size_error: "を%{max_size}以下のサイズにしてください"

参考: https://github.com/carrierwaveuploader/carrierwave-i18n/blob/master/rails/locales/ja.yml

コントローラーで使用言語を指定する。言語の指定方法はいくつかある。application.rbやapplication_controller.rbで絶対にlocaleをセットするようにするなど。以前多言語化した経験があるので、また記事で書きたいと思う。今回は、CarrierWaveの仕様確認が目的なので、さくっとできる方法で記述。

# /app/controllers/users_controller.rb

class UsersController < ApplicationController
  # 省略
  before_action :set_locale
  # 省略
  private
  def set_locale
    I18n.locale = :ja
  end
end

実行してみる

ja.ymlで設定した内容が表示される

英語は、動作からも分かるように設定しなくてもデフォルトでCarrierWaveのライブラリから読み取るが、変更したい場合は、en.ymlに同じように書けば良い。コピペして、変更すれば使える。

# /config/locales/en.yml

en:
  errors:
    messages:
      carrierwave_processing_error: failed to be processed
      carrierwave_integrity_error: is not of an allowed file type
      carrierwave_download_error: could not be downloaded
      extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
      extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
      content_type_whitelist_error: "You are not allowed to upload %{content_type} files, allowed types: %{allowed_types}"
      content_type_blacklist_error: "You are not allowed to upload %{content_type} files"
      rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
      mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
      min_size_error: "File size should be greater than %{min_size}"
      max_size_error: "File size should be less than %{max_size}"

ファイル容量を制限したい

サービスを運営する側としては、ストレージのコストの問題は尽きない。ファイルのアップロードというのは、本当にストレージの容量を食ってしまう。そんな時の対象方法。

用途によってファイルの上限と下限を決めるといい。今回のプロフィールなども含め画像は2MBだとぐらい適切かと思う。もっと少なくてもいいが、もっと少なくする場合は、アップロード時にファイルを圧縮する処理を入れないと、UX的に実用に耐えないと思う。

# /app/uploaders/image_uploader.rb
 
  def size_range
    0..2.megabytes
  end

2.5MBのファイルをアップロードしてみる

2.5MBのファイルを選択してUploadする
2MB以上だとエラーになる

大きさをリサイズして保存したい

MiniMagicを使用する。RMagicというライブラリも存在していて利用できるが、CarrierWave公式リポジトリにもMiniMagicを推奨すると書いている。

サーバー(アプリじゃなくてサーバー本体)にImageMagick(MiniMagickのコアロジックが入っている)をインストールする。

少なくとも今回のバージョンではmini_magickのgemのインストールは不要。Gemfile.lockを見ればわかるが、mini_magickがcarrierwaveと一緒にインストールされている。

AWS Linuxの場合

$ sudo yum install ImageMagick

Mac(OSX)でhomebrewを使っている場合

$ brew install imagemagick

アップローダーにリサイズの記述をする。下記のように記述することでアップロードする画像のサイズを常に200×200に設定できる。

2020/05/14追記
uploaderにMiniMagickをincludeする記述する旨を追記。これがないと、resize_to_fitというメソッドがありませんというエラーが出る。

# /app/uploaders/image_uploader.rb
  include CarrierWave::MiniMagick
 
  process resize_to_fit: [200, 200]

アップロードの前後の比較。Macのプレビュー画面で確認している。アップロード後は引き伸ばして大きく表示しているので、画像が荒いこともわかる。

アップロード前は640×640
アップロード後は200×200

ImageMagicがサーバーにインストールされていない状態だと、DBに更新をかけるタイミングで下記のようなエラーが表示される

ImageMagickがインストールされてない時のエラーメッセージ

リサイズの機能を使えば、ファイル容量を制限する時に更なる容量の制限もできると思う。

リサイズはバージョン保存と組み合わせると、サイズ違いの同じ画像が保存できる。先ほどの例に加えてversion指定でリサイズを記述する。

# /app/uploaders/image_uploader.rb

  process resize_to_fit: [200, 200]

  version :thumb do
    process resize_to_fit: [50, 50]
  end

同じようにアップロードをするだけで2枚の画像が保存される。デフォルトの状態では、[version名]_[元のファイル名]で保存される。

利用用途としては、例えば、プロフィール画像を、プロフィール情報の閲覧とメッセージ画面で利用する場合、プロフィール情報を表示する場合は、表示する枚数は1枚でサイズも大きめなので、通常の画像を表示する。一方、メッセージ画面は表示される回数も多く、画面も小さいので、読み込み速度を考慮すると、画像の大きさは必要最低限でいい。こんな場合に利用できる。

今回のケースでは、resize_to_fitというメソッドだけの記述だが、用途に応じたリサイズをする場面が実際には多いと思う。縦横比率を維持してリサイズしたいなど。。。
それは別途記事を書きたいと思う。

まとめ

基本的なところは上記のようなところだと思う。バリデーションの時にファイルを保持したまま更新とか必要最低限の記述を改めて確認できたのは非常に良かった。
エラーメッセージの多言語化も別gemでテンプレートがあったりして非常に良かった。

だが、業務レベルだと、S3にアップロードしたいとか複数枚のアップロードに対応したいとかある。その辺も試して別記事で残そうと思う。

コメント