Dragonfly attachmentを持つレコードをコピーする場合には注意が必要

Dragonfly については以前紹介した通りで、普段触っているアプリではこれを使っている。表示のタイミングで変換が走るのは高負荷のサイトには向かないかもしれないけど、使い勝手には満足している。

で、今回はこの Dragonfly を使っている場合に気をつける必要のあるユースケースを見つけたのでまとめておこうと思う。

ActiveRecord + Dragonflyでの画像の削除のタイミング

Paperclip もそうかもしれないけど、Dragonfly はよくできていて

  • レコードの削除
  • レコード内の Attachment の変更

のタイミングで Attach していたファイルをちゃんと掃除してくれる。これによって無駄なファイルが残ってしまうといったことがなくなる。

そしてこの処理は ActiveRecord で言うと

before_save

のタイミングで実行される。

削除時にはファイルの参照はチェックしないので自前で

今回ハマったのは

  • コピーしたレコードで
  • 画像を変更したら
  • 他のレコードも参照していたファイルが消えてしまった!

というものです。

上の説明を読んでいればとても当たり前の話なんだけど、まぁそこはそれ。回避するには以下のいずれかの方法がありそう。

  1. 参照カウンタよろしく、同一の uid を持つレコードが before_save の段階で 2以上あったら削除を無効化
  2. コピーの段階で実体のファイルもコピー

今回使ったのは 1 の方法。

削除を無効化する具体的な方法

※ 以下のコードは dragonfly 0.9.12 のものであり、また回避用のコードは実際に動いているものとよく似ているけれど動作検証はしていません。

dragonfly の画像の削除は知らない間に行われるので、callback で処理しているに違いない。ということで、before_* で検索してみる。

すると

lib/dragonfly/active_model_extensions/class_methods.rb

           before_save :save_dragonfly_attachments
           before_destroy :destroy_dragonfly_attachments

こんな記述が見つかる。探すと今度は

lib/dragonfly/active_model_extensions/instance_methods.rb

     def save_dragonfly_attachments
       dragonfly_attachments.each do |attribute, attachment|
         attachment.save!
       end
     end

     def destroy_dragonfly_attachments
       dragonfly_attachments.each do |attribute, attachment|
         attachment.destroy!
       end
     end

という記述が見つかる。これはそれぞれ

lib/dragonfly/active_model_extensions/attachment.rb

の中にある

     def destroy!
       destroy_previous!
       destroy_content(uid) if uid
     end

     def save!
       sync_with_model
       store_job! if job && !uid
       destroy_previous!
       self.changed = false
       self.retained = false
     end

のようだ。destroy! は期待通りに動いているので、save! でも同様に呼び出されている

     def destroy_previous!
       if previous_uid
         destroy_content(previous_uid)
         self.previous_uid = nil
       end
     end

があやしい。そこでこんなことをしてみた。

class Photo < ActiveRecord::Base

  image_accessor :image

  after_validation do
    if self.image_uid_was and
       self.class.where(:image_uid => self.image_uid_was).count > 1
      self.image.instance_eval {
        def destroy_previous!
          ;
        end
      }
    end
  end

end
  • 例として Photo という Model を用意し
  • その中の image という attribute に画像を保存
  • ファイルを変更しようとしており(photo_uid_was が nil でなかったら)、同じ uid を参照しているものが他に存在していたら
  • destroy_previous! を無効化

している。

今まさに保存しようとしているインスタンスだけ destroy_previous! の中身を空にしてしまう黒魔術。

すでに入っている before_save の callback の前にこれを追加することができるならそれでいいと思う。やり方が分からなかったので、before_save よりも前に呼ばれる after_validation に突っ込んだ。

after_validation から呼ぶなら before_save そのものを skip するという手も使えそうだけど、他に callback をセットしてるとそれもややこしいので、いちばん影響なさそうで手っ取り早い黒魔術で片付けることにした。

これでコピーしたレコードの画像を差し替えても他のレコードでロストしない。

More

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Develop Document Jekyll