file_column プラグイン内部構造
file_column の内部構造についてメモ。
Rails におけるファイルアップロードの仕組みについて。
まず file_column を使わずにどうやってファイルをアップロードするのかというところから始める。
Entry モデルに image というファイル用のカラムがあったとする。アップロード用のフォームは次のような感じ。
<% form_tag({:action => 'create'}, :multipart => true) do %> <%= file_field 'entry', 'image' %></p> <% end %>
file_field() は という HTML を生成するヘルパメソッドである。form_tag() で :multipart => true と指定されていることに注意。これがないとアップロードできない。
アップロードされたファイルを受け取るコントローラ側では・・・
def create @entry = Entry.new(params[:entry]) logger.debug "EntriesController#create @entry.image.class.name, @entry.image = #{@entry.image.class.name}, #{@entry.image.inspect}" # => EntriesController#create @entry.image.class.name, @entry.image = Tempfile, #<File:/tmp/CGI.3002.1> if @entry.save flash[:notice] = 'Entry was successfully created.' redirect_to :action => 'list' else render :action => 'new' end end
という感じ。 Rails がいろいろ賢くやってくれて、@entry.image にはアップロードされたファイルを表現する Tempfile オブジェクトが入っている。Tempfile を実際にしかるべきパスに保存するという操作を自分で書かないといけない。これを代わりにやってくれるのが、file_column プラグインである。
モデルに file_column
file_column プラグインの使い方の基本は、モデルで file_column() を使うことだ。また例によって Entry#image というカラムを考える。
class Entry < ActiveRecord::Base file_column :image end
これで、image カラムをファイル格納用のカラムに指定したことになる。ではこの file_column はいったい何者か? $RAILSAPP/vendor/plugins/file_column/lib/file_column.rb に file_column は定義されている。
# $RAILSAPP/vendor/plugins/file_column/lib/file_column.rb に file_column より抜粋 def file_column(attr, options={}) state_attr = "@#{attr}_state".to_sym state_method = "#{attr}_state".to_sym define_method state_method do #(A) result = instance_variable_get state_attr if result.nil? result = FileColumn::create_state(self, attr.to_s) instance_variable_set state_attr, result end result end define_method "#{attr}=" do |file| #(B) state = send(state_method).assign(file) #(C) instance_variable_set state_attr, state #(D) end end
本筋に関係ない部分は大幅に割愛した。重要なのは、file_column :image は Entry#image=() というメソッドを定義していることだ。(B) 代入が起こった瞬間に file_column はファイル保存を行っているのだ。
上のコードで attr = "image" であることを考慮すると、上のコードは次のようなメソッド群を定義していることになる。
class Entry < ActiveRecord::Base def image_state result = @image_state if result.nil? result = FileColumn::create_state(self, "image") @image_state = result end end def image=(file) state = image_state.assign(file) @image_state = state end end
Entry#image に Tempfile を代入しようとするとき、まずは上で定義された image=() が呼ばれる。内部で image_state() が呼び出され、この戻り値に対して assign(file) が呼び出される。image_state() は内部で FileColumn::create_state(self, "image")を呼び出している。FileColumn::create_state() を見てみる。
# $RAILSAPP/vendor/plugins/file_column/lib/file_column.rb def self.create_state(instance,attr) filename = instance[attr] if filename.nil? or filename.empty? NoUploadedFile.new(instance,attr) else PermanentUploadedFile.new(instance,attr) end end
instance[attr] はこの場合、entry["image"] である。entry["image"] にまだ値が入っていないとすると、NoUploadedFile インスタンスが返ることになる。
実は NoUploadedFile 以外にも、いくつか XXXUploadedFile と呼ばれるクラスが定義されている。
具象クラスはアップロードファイルを処理すると同時に、その状態をあらわすオブジェクトにもなっている。
NoUploadedFile | ファイルのない状態 |
TempUploadedFile | アップロードされたものが一時ファイルとして存在する状態 |
PermanentUploadedFile | アップロードされたものが永続ファイルとして存在する状態 |
を表している。メソッド呼び出しによって状態が遷移する。
上で見たように create_state() によって初期状態が作られる。初めてファイルをアップロードするときには、No(UploadedFile) 状態となる。image=() の state = image_state.assign(file) では、次に assign() が呼ばれる。assign() はソースを見るとわかるが実質 upload() と同等であるから、Temp(UploadedFile) オブジェクトが返ってくる(=Temp 状態に遷移する)。そして、Entry オブジェクトが保存されるとき、after_save コールバックが呼び出され、ここで Permanent(UploadedFile) 状態へさらに遷移する。このとき、アップロードされたファイルは、一時フォルダから永続フォルダへ移動される。(after_save コールバックと TempUploadedFile#after_save との紐付けは FileColumn.file_column で行われている。ソース参照)