Ruby on Rails: ファイルアップロード機能を作る (ajax_scaffold_generator 編)

Posted in rails by o-taki on the 2007/3/31

前回の記事で作ったファイルアップローダーは、ファイル本体をバイナリデータとして DB に保存したが、DB ではなくファイルとしてサーバに置くものを作ってみる。
それだけではつまらないので、今回は ajax_scaffold_generator を使って見栄えのいいものを作ってみることにした。

■ バージョン情報

$ ruby script/about
./script/../config/boot.rb:29:Warning: require_gem is obsolete.  Use gem instead.
About your application's environment
Ruby version                 1.8.5 (i686-linux)
RubyGems version             0.9.2
Rails version                1.2.2
Active Record version        1.15.2
Action Pack version          1.13.2
Action Web Service version   1.2.2
Action Mailer version        1.3.2
Active Support version       1.4.1
Application root             /home/y-ohtaki/rails/ajax_uploader
Environment                  development
Database adapter             mysql
Database schema version      1

初期設定

■ 準備

ajax_scaffold のインストール。

$ gem install ajax_scaffold_generator

ファイルを保存するためのフォルダを作っておく。

$ mkdir files

■ rails してから scaffold 作成まで

$ rails ajax_uploader

データベース設定。とりあえずデータベースにはファイル名のみ保存。

$ ruby script/generate migration create_attachments

migrate は次のような感じ。

$ cat db/migrate/001_create_attachments.rb
class CreateAttachments < ActiveRecord::Migration
  def self.up
    create_table :attachments do |t|
      t.column(:filename, :string)
    end
  end

  def self.down
    drop_table :attachments
  end
end

migrate する。

$ rake db:migrate

scaffold 作成。

$ rails script/generate ajax_scaffold attachment

ビューの修正

結構めんどくさい。

■ ファイルをダウンロードするためのリンクを作る

ajax_scaffold を使うと edit という項目が自動的につくけど、アップロードしたファイルを edit するようなことは想定しないので、ここをダウンロードリンクとして使うことにする。

_attachment.rhtml の link_to_remote を

         <%= link_to "Download", edit_options %>

に変更。

本当はメソッドも download とか view_file とかにするべきなんだけど、めんどくさいのでメソッド名は edit のまま(ダメ仕様)

■ 新規登録 form 修正

scaffold で生成された新規登録 form は text_field になっているが、実際にはファイルをアップロードするので、_form.rhtml の

<%= text_field 'attachment', 'filename' , {:class=>"text-input"} %>

の部分を

<%= file_field 'attachment', 'filename' , {:class=>"text-input"} %>

に変更。

コントローラでは、ここで選択されたファイルからファイル名を抜き取ってデータベースに入れるようにする(後述)。

■ 新規登録 form で multipart を扱えるようにする

ここがめんどくさい。ajax_scaffold で生成される form_remote_tag では :multipart を使えない。
理由は、javascript ではローカルファイルを扱えないから。

【参考】 Can you upload files with AJAX?

しょうがないので responds_to_parent を使うことにする。

【参考】 Ajaxっぽく画面遷移なしでファイルアップロードしたい! (yamazのRails日記)

$ ruby script/plugin install http://sean.treadway.info/svn/plugins/responds_to_parent/

responds_to_parent の使い方

  • ビューに iframe を用意して、form_start_tag の :target にその iframe を指定しておく。
  • form_start_tag で呼ばれるアクションに対応したメソッド内で responds_to_parent メソッドを使う。
  • responds_to_parent メソッドの引数にブロックを渡す。ブロック内では render :update や rjs を使って javascript を render するようにする。

responds_to_parent の動作

  • 渡された“render の内容 (response)” = “実行したい javascript” が、親要素を基準として実行されるよう処理をする。
  • 要は location をいじってるだけだったりする。
  • 渡された javascript を実行した後に、location を再設定するための javascript を setTimeout で呼んでる。
  • なので、setTimeout のコールバックが呼ばれる前にresponds_to_parent を処理する iframe を消したりするとスクリプトエラーになる。

■ responds_to_parent 用の iframe を追加する

ajax_scaffold では、新規追加や変更時の form は submit されると消去されるようになっているので、form 内に iframe を入れとくと前述の setTimeout の関係ではまります。注意。(←はまった)。

今回は component.rhtml 先頭部分に iframe を作ることにする。

<% if @show_wrapper %>
<iframe id='iframe_for_responds_to_parent' name='iframe_for_responds_to_parent' style="display:none;"></iframe>

■ ファイルアップロードフォーム修正

_new_edit.rhtml を変更。form_remote_tag を start_form_tag にする。

修正前

    <%= form_remote_tag :url => @options.merge(:controller => '/attachments'),
          :loading => "Element.show('#{loading_indicator_id(@options)}'); Form.disable('#{element_form_id(@options)}');",
          :html => { :href => url_for(@options.merge(:controller => '/attachments')),
          :id => element_form_id(@options) } %>

修正後

    <%= start_form_tag(@options.merge(:controller => '/attachments'),
          {:id => element_form_id(@options),
           :multipart => true,
           :target => "iframe_for_responds_to_parent",
           :onsubmit => "submit();Element.show('#{loading_indicator_id(@options)}'); Form.disable('#{element_form_id(@options)}'); return false;"}) %>

form_remote_tag の :loading は Ajax で要求をした後に実行されるようになっているので、これを start_form_tag の :onsubmit にそのまま持ってくるとまずいことになることがある。(onsubmit は POST (or GET) の前に実行される)

ここでは、Form.disable が POST の前に動いて、form の内容を全部無効にしてしまうので、POST する時に要求データが空になってしまう。 (なぜか 404 になった)
これを避けるために、:onsubmit 内で最初に submit(); して POST しておき、return false; で :onsubmit 終了時の POST を抑制する。

コントローラの修正 (attachments_controller.rb)

ファイルパスを定数で定義。以下を追加。

FILE_PATH = "#{RAILS_ROOT}/files"

■ edit メソッドを修正する

edit メソッドでファイルダウンロードさせるようにしたいので、render せずに send_file する。

return render(:action => 'edit.rjs') if request.xhr?

を削除。

      @options = { :scaffold_id => params[:scaffold_id], :action => "update", :id => params[:id] }
      render :partial => 'new_edit', :layout => true

も削除。

代わりに

      file = "#{FILE_PATH}/#{params[:id]}"
      send_file(file, :filename => @attachment.filename,
                :stream => false,
                :disposition => 'inline')

を追加。

■ create メソッドを修正する

begin の後に set_file_name を追加。

set_file_name メソッドでは、まずファイル本体をインスタンス変数に格納してから、params の :filename をファイル名に書き換える。

@successful = @attachment.save の後に save_file を追加。

save_file メソッドは、名前の通りファイルを保存するメソッド。

んで、

return render(:action => 'create.rjs') if request.xhr?

return responds_to_parent do render(:action => 'create.rjs') end

に変更。

ajax_scaffold は javascript が使えない環境でもうまく表示されるようになってるんだけど、ここの render の部分でそれを見事にスポイルしてる。しかし、今回はとりあえず無視。

set_file_name メソッドと save_file メソッドを追加。

  private

  def set_file_name
    @file = params[:attachment][:filename]
    params[:attachment][:filename] = @file.original_filename
  end

  def save_file
    if @file
      File.open("#{FILE_PATH}/#{@attachment.id}", "w") do |f|
        f.write(@file.read)
      end
    end
  end

■ destroy メソッドを修正する

@successful = Attachment.find(params[:id]).destroy

      @attachment = Attachment.find(params[:id])
      if @attachment.attribute_present?('filename')
        File.delete("#{FILE_PATH}/#{params[:id]}")
      end
      @successful = @attachment.destroy

に変更。

これで一応完成。
ajax_scaffold はカスタマイズするには結構な労力がかかることがわかった。(特にビュー)

以上

Comment

Loan2007/6/3 00:18

vostorg, pefrect!

dddd2008/11/15 14:26

www

coorgigue2009/4/21 06:45

Hello, please, help.
What should i do about this?

Thenks, bro. I am vaiting for answer!!!

coorgigue2009/4/22 05:10

Hi Man!. Just one more question. Realy, please, help me.
Is 5 days long enough 2 get alcohol out of your urine for a urine test?

Thenk you. I am vaiting for answer!!!

Trackback URL