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 で行われている。ソース参照)

まとめ

ソースコードを逐行解説しようかと思ったが、あまりに面倒なのでやめた。ソースコードなんて誰も読みたくないと思うし。上の図の方が重要。要するに状態機械としてクラス群を定義しているのがわかれば、ソースはなんとか読めるだろう。