はじめに
- pomeruというサービスを開発しているエンジニアです。
- 最初はユーザーのアイコン画像を、Twitterのアイコン画像へのURL(こんなの)をそのまま所持する形でDBに格納していたのですが、それだと画像を自分で変えることができないので、途中からCarrierwaveで管理するという方式に切り替えることになりました。
- 今回はその時の珍しい経験が少しでも同じような境遇の人のお役に立てればと思い、まとめました。
環境
$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]
$ rails -v
Rails 5.2.1
最初の状態
-
Deviseで作成した
User
モデルにusericon
というフィールドを持たせていて、そこにtwitterアイコンのURLを格納していました。
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
add_column :users, :usericon, :string
end
# サインアップ時に、URLをDBに保存
def find_for_twitter_oauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
# 中略
user.usericon = auth.info.image # アイコンへのURLが入っている
end
end
<% # ビューから呼び出すとき %>
<img src="#{@user.usericon}" />
目指す最終形
-
Carrierwave
とfog
を使い、画像をS3へ格納する形にします。
# Gemfile
gem 'carrierwave'
gem 'mini_magick'
gem 'fog-aws'
- uploaderモデルをUserモデルにマウントさせます。
# uploaderモデルを作成する
# rails g uploader usericon
# app/uploaders/usericon_uploader.rb
class UsericonUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
if Rails.env.development? || Rails.env.test?
storage :file
else
storage :fog
end
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def default_url(*args)
'/images/fallback/profile_icon.png'
end
process resize_to_fill: [300, 300]
process convert: 'png'
def extension_whitelist
%w[jpg jpeg gif png]
end
def filename
super.chomp(File.extname(super)) + '.jpg' if original_filename
end
end
# user.rb
mount_uploader :usericon, UsericonUploader
# サインアップ時の所作をCarrierwave対応
def find_for_twitter_oauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
# 中略
user.remote_usericon_url = auth.info.image
end
end
<% # ビューから呼び出すときもCarrierwave対応 %>
<img src="#{@user.usericon_url}" />
切り替えに必要な処理
- 切り替えるに当たって、以下のことをクリアする必要があります。
- 現状のユーザー全員の画像をS3に保存する。
- それぞれの画像の保存先のURLをCarrierwaveを介してDBに保存する。
- これ以降、サインアップした人は自動的にTwitterのアイコン画像をS3に保存されるようにする。
- Carrierwaveに対応したコードを本番環境に反映する。
- 厄介なのはこの順番で、最初からいきなりCarrierwaveをマウントしてしまうと、現在格納されているアイコン画像へのURLがおかしくなってしまいます。
- なので、上をクリアするために、具体的な動作として以下の道筋を踏みました。
UsericonUploader
を作成して、User
モデルへマウントする。- ログイン時や表示する時の画像へのアクションを、Carrierwave対応する。
- 上のコードを
feature/image_to_s3
ブランチとして切っておき、いつでもmaster
ブランチから切り替えられるようにしておく。- 本番環境をメンテナンスモードへ移行。
- 本番環境のDBデータのバックアップを取り、ローカルへダウンロード・反映させる。
- ローカルで全てのユーザーの「ID」と「画像へのURL」をCSVに書き出すタスクを走らせる。(ブランチは
master
)- ローカルでブランチを
feature/image_to_s3
に切り替え、CSVを順次読み込んでS3へ画像データを保存するタスクを走らせる。(carrierwaveのストレージをこの時だけfog
に変えておくこと!!)- ローカルの書き換わったDBデータをdumpし、本番環境のDBに反映する。
master
ブランチにfeature/image_to_s3
をマージして、本番環境へpushする。
- 実際に使ったタスクファイルは以下のようなものです。
# 最初に全てのUserのIDと画像URLをCSVに書き出すタスク
namespace :get_twitter_image_urls do
desc 'get and save twitter_image_urls'
task run: :environment do
f = File.open('twitter_image_url.csv', 'w')
User.all.each do |u|
# Carrierwaveをマウントする前に実施すること(マウントしたあとだと、URLが変わってしまうため)
f.puts "#{u.id},#{u.usericon}"
end
end
end
# 書き出したCSVを元に、画像をS3へ保存するタスク
# Carrierwaveのストレージを、development環境でもfogを使うように設定しておくこと(fileだとローカルに保存されてしまうため。)
namespace :save_images_to_s3 do
desc 'save twitter_image_urls to s3'
task run: :environment do
require 'csv'
ActiveRecord::Base.transaction do
CSV.foreach('twitter_image_url.csv') do |row|
begin
u = User.find(row.first)
u.remote_usericon_url = row.last
if u.save
Rails.logger.debug("#{u.id}:#{u.nickname} saved")
else
# NOTE: URLに画像が存在しなかった場合は、デフォルトの画像を使うために画像を削除する。
u.remove_usericon!
# HACK: どういうわけかvalidationを切らないと保存してもらえない(私だけだろうか?)
u.save(validate: false)
end
rescue StandardError => e
Rails.logger.debug(e.message)
next
end
end
end
end
end
注意点
DBのバックアップとメンテナンス
- 言わずもがなですが、切り替え中はDBへの更新を防ぐため、メンテナンスモードにしましょう。
- そしてデータをいつでもリストアできるよう、バックアップも取っておいてください。
- なお今回は
Heroku
で動かしていたため、メンテナンス・バックアップ・リモートとローカル間でのデータのやり取りがとても楽でした!
# メンテナンスモード移行
$ heroku maintenance:on
# バックアップをとる
$ heroku pg:backups capture
# 最新のバックアップファイルをローカルへ(デフォルトで`latest.dump`という名前になります)
heroku pg:backups:download
# ローカルへダウンロードしたDBデータを反映する
$ pg_restore --verbose --clean -d app_development latest.dump
# 加工したローカルのDBデータをダンプする
$ pg_dump app_development > dump.sql
# heroku環境に反映する(向こうのデータは一旦リセットしないと入らないこともあるので注意)
$ heroku pg:psql DATABASE_URL < dump.sql
# 万が一前のデータをバックアップを使ってリストアしたくなった場合
heroku pg:backups:restore b101(バックアップの識別子) DATABASE_URL
# メンテナンスモード解除
$ heroku maintenance:off
Carrierwaveをマウントするタイミング
- 度々書いていますが、Carrierwaveをマウントする前と後で、
usericon
を呼び出した時の返り値が変わってしまうため、意図したデータを取得・保存させるためには適切なタイミングで切り替える必要があります。 - 今回の場合は、最初
usericon
に画像へのURLが入っていて、その一覧を取得する必要があったので、Carrierwaveをマウントする前にURL一覧をCSVに書き出しました。 - そして然る後、Carrierwaveをマウントして、ストレージを
fog
にした状態でタスクを回し、CSVの情報を元に画像をS3へ保存する処理を行いました。 - この順番をしっかりと守るようにしてください。
ネット環境の確保
- 今回は3000を超えるユーザーデータを処理したため、時間的には30分以上かかってしまいました。
- やはりS3との通信を確保するために安定したネット環境は必須です。(今回の場合はローカルで処理しているので)
おわりに
- 今回はイレギュラー中のイレギュラーで大変でしたがなんとかなりました。
- が、なんにせよ一番大切なのは最初からCarrierwaveを使うか使わないかをしっかり議論して、使うのなら最初から使うのがベストなんだなぁと再認識しました。