839の日記

趣味の話を書くブログです。

carrierwaveからactivestorageに移行した

最近画像管理にcarrierwaveを使っていたものをactivestorageに移行した。 移行のモチベーションはローカルに置いていた画像をGCSとかに置きたいなと思ったが、どうせ対応するならactivestorageに移行してからにしようかと思ったため。 今回はactivestorageに移行したがファイルはlocalに置いたままで、GCSに置くのはまたGKEに移すときにでも考えようと思っている。

5系のrailsで移行しようとしていたが、そろそろ6が出る && 6系で破壊的に変わる部分があるというのをissueでちら見していたので一応activestorageのmigrationファイルを確認。 当たり前だけど、DBに関しては破壊的変更はなさそうだったのでそのままやることにした。

移行にあたってはとりあえずraiseはしないことだけ主目的に移行した。 業務だったらもう少しいい感じにやらないといけないが、個人サービスなので多少UXが下がる瞬間があっても良いかな、という気持ち。

  1. carrierwaveで管理している画像をactivestorageに移行するためのメソッドを生やす
  2. 新規で画像が取り込まれるときにはactivestorage側に保存するようにする
  3. 画像を表示する部分はactivestorageで画像が管理されているものを表示するようにする
  4. capistranoを使っているならlinked_dirsでpublic/uploadsを指定していると思うが、storageに変更しておく(localに置いている人限定)

一旦ここまでやってデプロイ。 移行するためのメソッドは具体的には以下のようなものを生やした。

class User < ApplicationRecord
  mount_uploader :image, ProfileImageUploader
  has_one_attached :avatar
  
  def self.migrate_as!
    failed_user_ids = []
    where.not(image: nil).find_each do |user|
      next unless user.image.url

      user.avatar.attach(io: open(user.image.file.file), filename: 'avatar.png') # URI.openを使ったほうが無難
    rescue Errno::ENOENT
      failed_user_ids.push(user.id)
      user.remove_image!
    end
    failed_user_ids
  end
end

user.image にcarrierwaveをぶら下げていたが、たまになぜかimageがある扱いなのにないやつがいたので rescue Errno::ENOENT で救う。 今回はそういう人たちは画像はなかったこと扱いにした。 が、一応念のためにidだけメモっておきたかったので配列で確保しておく。

デプロイした後は上記メソッドを速やかに叩いて表面上の移行は完了。 ただし、ちゃんとやるなら以下のような点を注意する必要があるなと感じた。

  1. デプロイされる
  2. user Aが新しく画像を設定(activestorageに保存される)
  3. 移行スクリプトを叩く => user Aの画像がcarrierwave時代のものに戻ってしまう

まぁメンテナンスタイムを設けるのが無難だとは思う。

次に残ったcarrierwaveをいろいろと抹消していく。

  • carrierwaveで使っていたカラムの削除(今回であればuser.imageを消すmigrationファイルを書く)
  • uploads系のファイルとかを含めたcarrierwave依存のロジックを消す
  • デプロイ先のpublic/uploads削除(localに置いている人限定)
  • .migrate_as! の削除

今回ちょっと手間だったのは user.image.urlAPI経由で返している場所。 viewで使う分には

= image_tag(url_for(user.avatar)) if user.avatar.attached?

と書けばいいが、modelでurlを返すメソッドを生やしてるとこうは書けない。 自分の場合は以下のように書いて対応した。

Rails.application.routes.url_helpers.rails_blob_path(avatar, disposition: :inline, only_path: true)

only_path: true をつけているので、host名は含まれずにpathだけで返される。 なのでurlを実際に使う側でいい感じにhost名を補完する必要がある(もちろん上記にhost名を返すように追加ロジックを書いてもいい)。

おまけ

carrierwaveを消す際にactivestorageではいろいろテーブルを追加してファイルを管理しているが、carrierwaveではどのように管理しているのかが気になった。 今回はimageというカラムを生やしていたので、imageにいろいろ情報が突っ込まれてるのかな?と思ったがstring型である。 string型ということはserialize hashとかにして管理してるのか…と思って中身を見たら image という文字列しか入ってない。

これはどういうことなんだろう、と思ってuploaderの実装(数年前なので全然覚えてなかった)を見に行ったら以下のような感じだった。

class ProfileImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick
  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

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

カラムがnilじゃなかったらstore_dirから探索してファイルを置くような感じっぽい。 少しドキュメントを読んでみた感じだとserialize jsonとしてデータを保存することもできたようだ。

脳死でとりあえずcarrierwaveを入れてしまっており、挙動をあんまり理解していなかったのでactivestorageではもう少しこの辺も意識していきたいと思う。