CarrierWaveでアップロード済みのファイルを消さないようにする方法
とあるcarrierwaveを使った案件で、フラグによるファイルの削除やレコードの削除でアップロード済みのファイルが消されないようにしたいという要件が発生しました。
carrierwaveではデフォルトでフラグによるファイルの削除、レコードの削除をすると当然一緒に実ファイルが削除されてしまいます。
そこで今回はどういった条件でファイルが削除されてしまうのか確認しつつ、回避方法を紹介します。
今回の案件に合わせてバージョンは1.2.3で話を進めていきます。
フラグによるファイルの削除
carrierwaveでは mount_uploader を定義した時点で remove_#{column}=(value) と言うアクセッサが追加されます。そしてチェックボックスなどで値を設定すると削除するというフラグが設定されます。
# lib/carrierwave/mount.rb
def remove_#{column}=(value)
_mounter(:#{column}).remove = value
end
そしてコールバック処理が動き出します。(ここでは削除に関係のある動きのみ抜粋します。
まずは write_#{column}_identifier が動き出し、 write_#{column}_identifier でマウントしたカラムをクリアします。
# lib/carrierwave/orm/activerecord.rb
before_save :"write_#{column}_identifier"# lib/carrierwave/mount.rb
def write_#{column}_identifier
return if frozen?
mounter = _mounter(:#{column})
if mounter.remove?
write_uploader(mounter.serialization_column, nil)
elsif mounter.identifiers.first
write_uploader(mounter.serialization_column, mounter.identifiers.first)
end
end
次に remove_previously_stored_#{column} が動いて、マウントしたカラムが空になっているということでファイルを削除します。
# lib/carrierwave/orm/activerecord.rb
after_commit :"remove_previously_stored_#{column}", :on => :update# lib/carrierwave/mount.rb
def remove_previously_stored_#{column}
before, after = @_previous_changes_for_#{column}
_mounter(:#{column}).remove_previous([before], [after])
end# lib/carrierwave/mounter.rb
def remove_previous(before=nil, after=nil)
after ||= []
return unless before
# both 'before' and 'after' can be string when 'mount_on' option is set
before = before.reject(&:blank?).map do |value|
if value.is_a?(String)
uploader = blank_uploader
uploader.retrieve_from_store!(value)
uploader
else
value
end
end
after_paths = after.reject(&:blank?).map do |value|
if value.is_a?(String)
uploader = blank_uploader
uploader.retrieve_from_store!(value)
uploader
else
value
end.path
end
before.each do |uploader|
if uploader.remove_previously_stored_files_after_update and not after_paths.include?(uploader.path)
uploader.remove!
end
end
end
既にご存知の方、コードを呼んでおやっと思った人もいるかも知れません。
そうです。 remove_previously_stored_files_after_update というパラメータに false を設定してあげれば防げるのです。
実は wiki にもその情報が書かれています…
それでは気を取り直して、レコードの削除によるファイルの削除を見ていきたいと思います。
レコードの削除によるファイルの削除
フラグの時と同様にコールバックで削除が実行されます。
remove_#{column}! が呼ばれ、マウントしたカラムを空にします。
さらに super を使うことで継承ツリーの上位に差し込んだモジュールのメソッドを呼び出しファイルを削除しています。
# lib/carrierwave/orm/activerecord.rb
after_commit :"remove_#{column}!", :on => :destroydef remove_#{column}!
self.remove_#{column} = true
write_#{column}_identifier
self.remove_#{column} = false
super
end# lib/carrierwave/mount.rb
def remove_#{column}!
_mounter(:#{column}).remove!
end
コールバックで削除が実行されていると聞いてピンと来た方は素晴らしい。
skip_callback があります。そう、Railsならね。
すみません、こちらも実はREADMEに記載されていることです。
ただフラグによるファイルの削除も同様ですが、中身の挙動を知っていて使うのと、知らずに使うのではトラブルに見舞われたときの対処の仕方も変わってきます。
当然中身を理解していれば、すぐに当たりが付けられますが、挙動を知らなければ、そこから調べ直さなくてはいけません。
通常はあまりないケースかとは思いますが、今回は carrierwave による実ファイルの削除を抑止する方法について紹介しました。
carrierwave 今回読んだ部分はリフレクションを多用しているので、初めて読む人には分かり辛いかと思いますが、このコードを読むことができれば Ruby に対する知識もまた広がると思うので、ぜひ読んでみてください。
前回の記事同様、挙動をちゃんと理解することが大切ですよ、というお話でした。