見出し画像

Webアプリ tsunagaru をリリースしました [4] ActiveStorage から CarrierWave への移行

2/18 個人制作で、Webアプリ tsunagaru をリリースしました。

スマホの操作に慣れていない方や、療養中などで体が思うように動かない方も、1ボタンで簡単にメッセージを送ることが出来るチャットアプリです。
せっかくなので、その制作過程で得られたこと感じたことを書いておこうと思います。

今回は ActiveStorage から CarrierWave への移行について書きました。

ActiveStorage から CarrierWave へ

ActiveStorage とは、「クラウドストレージサービスへのファイルのアップロードとそれらのファイルをActive Recordオブジェクトに添付することを容易」にする Rails5.2 から入った機能で、かなりお手軽にファイルアップロード処理が実装出来ます。その反面、ファイルのキャッシュやバリデーションがサポートされていなかったりします。

CarrierWave は古くからあるファイルアップロードのための Gem です。Rails の古いバージョンでも利用でき、キャッシュやバリデーション、また画像サイズを小さくしてのアップロードや、サムネイル用の画像の生成が設定ファイルから簡単に行えるようサポートされています。

当初、ユーザーのプロフィール画像の管理・アップロードは ActiveStorage で実装していましたが、後述する理由のため、アップロード時にリサイズしたり、サムネイル用画像も簡単に作ってくれる CarrierWave へ移行しました。以下に ActiveStorage から CarrierWave への移行の際に対応した内容を記載します。

Gem のインストール

carrierwave、AWSサービスを利用するための fog-aws、画像処理を行う
rmagick を入れます。

# Gemfile
gem 'carrierwave'
gem 'fog-aws'
gem 'rmagick'

Users テーブルにカラムを追加

carrierwave では blobs など ActiveStorage の中間テーブルを使わず Users テーブルにカラムを追加して利用するため、そのためのマイグレーションを作成・実行します。

# 新しく生成したマイグレーションファイル
class AddAvatarToUsers < ActiveRecord::Migration[5.2]
 def change
   add_column :users, :avatar, :string
 end
end

実装の移行

各種実装を CarrierWave 用に置き換えます。

アップローダーの指定
アップローダーを作成し、指定します。ActiveStorage 利用時の has_one_attached は削除します。

# アップローダーの作成
$ bundle exec rails generate uploader avatar
# user.rb
mount_uploader :avatar, AvatarUploader

フォームにアップロードファイルの削除フラグを追加(任意)
アップロード済みのファイルを削除するかどうかのフラグをビューから送ります(任意)。
実装例として、font-awesome のゴミ箱アイコンをボタンとして表示しています。

# フォームのビュー
label
 span.filelabel.my-checkbox title=""ファイルを削除""
   = f.check_box :remove_avatar
   = fa_icon 'trash', class: 'icon'

フォームのパラメータの修正
画像ファイル、キャッシュ、削除するかどうかのフラグをストロングパラメータに追加します。

# users_controller.rb
def user_params
 params.require(:user).permit(:name, :email, :avatar, :avatar_cache, :remove_avatar)
end

画像表示処理の修正
アップロード時にサムネイル用画像も保存するので、普段はそちらを表示するようにします。

# user.rb
def avatar_or_default
 image.present? ? image.thumb.url : Settings.user.avatar.default_file_name
end

アップローダーの設定
ストレージやリサイズするサイズ、ファイル名などを指定します。
また、画像が90度回転した状態でアップロードされてしまうことへの
対応を fix_rotate で行なっています。

# avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
 include CarrierWave::RMagick
 # ストレージを指定
 if Rails.env.production? || Rails.env.staging?
   storage :fog
 else
   storage :file
 end
 # 保存先を指定
 def store_dir
   "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
 end
 # アップロードした写真が回転してしまう問題に対応
 def fix_rotate
   manipulate! do |img|
     img = img.auto_orient
     img = yield(img) if block_given?
     img
   end
 end
 process :fix_rotate
 # アップロード時にリサイズ
 process resize_to_limit: [800, 800]
 # サムネイル用にリサイズ
 version :thumb do
   process resize_to_fit: [200, 200]
 end
 # アップロード可能なファイルをホワイトリストで指定
 def extension_whitelist
   %w(jpg jpeg gif png)
 end
 # 保存するファイルの命名規則
 def filename
    "#{secure_token}.#{file.extension}" if original_filename.present?
 end

 protected
 
 # 一意となるように uuid メソッドから生成
 def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid)
 end
end

carrierwave の設定ファイルの変更
carrierwave.rb の本番環境での設定を追加します。

# carrierwave.rb
require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'
CarrierWave.configure do |config|
 if Rails.env.production?
   config.storage :fog
   config.fog_provider = 'fog/aws'
   config.fog_directory  = ENV['AWS_S3_BUCKET_NAME']
   # キャッシュの保存期間
   config.fog_attributes = { 'Cache-Control': "max-age=#{365.day.to_i}" }
   # リンク直アクセスを弾く
   config.fog_public = false
   # キャッシュもS3に置く
   config.cache_storage = :fog
   config.cache_dir = 'tmp/image-cache'
   
   config.fog_credentials = {
     provider: 'AWS',
     aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
     aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
     region: ENV['AWS_REGION']
   }
 else
   config.storage :file
   config.enable_processing = false if Rails.env.test?
 end
end
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

config.fog_public = false とすると、リンク直アクセスで画像が表示されなくなります。オープンにする必要が無いので false としました。
cache_storage を指定することでキャッシュもS3に保存出来るようになります(1台で動かす場合は必要無いかと思いますが)。

Twitter のプロフィール画像を carrierwave から S3 にアップロード
Twitter のプロフィール画像を取得して、ActiveStorage のオブジェクトにアタッチする実装については 以前 書いており、そちらでは save 後に open-uri を使って画像のダウンロードおよびアタッチを行うメソッド(download_and_attach_avatar)を自前で実装して呼び出していました。

# oauths_controller.rb
def create
  @user = User.new(user_params)
  if @user.save
    @user.download_and_attach_avatar
    redirect_to root_url, success: 'ユーザーを作成しました'
  end
end

carrierwave では「remote_{アップローダー用のカラム}_url」でダウンロード先のURLを指定して save を行うと、ダウンロードおよび指定されたストレージ(S3)へのアップロードを行なってくれます。

# oauths_controller.rb
def create
  @user = User.new(user_params)
  @user.setup_attach_avatar
  if @user.save
    redirect_to root_url, success: 'ユーザーを作成しました'
  end
end
# user.rb
def setup_attach_avatar
  return unless avatar_image_url
  # remote_{アップローダー用のカラム}_url
  self.remote_avatar_url = avatar_image_url
end

# Twitter のプロフィール画像のオリジナルサイズのパスを取得する
def avatar_image_url
  profile_image_url&.gsub(/_normal/, '')
end

実装を見ると この辺り に記載があります。
何となく程度でしか全体の流れは追えていませんが、
mount_uploader の定義により mounter の生成や「remote_#{column}_url」などのメソッドが生え(ソース)、uploader を通して download し(ソース)、save 時には after_save で「store_#{column}!」が呼ばれ(ソース)、uploader を通して「storage.store!」から指定のストレージへアップロード(ソース)という流れでしょうか。しっかりとモジュールで責務が分かれていて勉強になりますね。

あとがき

最初は ActiveStorage で手早く実装してリリース、と思っていましたが、ファイルサイズを小さくしてアップロード出来なかったため、数MBのファイルをそのままアップロードする実装でした。それはまずいとカスタムバリデーションを作成して1MB以上はアップロード出来ないようにすることも出来ましたが、下記のような利用シチュエーションを考えると問題があることが分かりました。

ユーザー登録する > 自身のアバター画像が設定出来ることを知る > カメラで撮る > 高精細なためファイルサイズが大きくなり、バリデーションエラー

これを解消するには何らかアプリやPC上で解像度を編集する必要があります。もしくは解像度が低いフロントカメラ(セルフィー)なら収まる場合があります。
しかし普通の事が普通に出来ないのはストレスですし、このアプリの用途を考えると他のアプリに比べてアバター(顔)画像の重要性が高いと考えます。そのためスマホで撮った写真が何も考えず使えないのは致命的と感じ、CarrierWave へ移行しました。
一度実装したものを手段を変えて実装し直すのは少しためらいがありましたが、何が大事か検討した上でちゃちゃっと対応出来て良かったと思います。

この記事が気に入ったらサポートをしてみませんか?