2009年10月30日金曜日

Rails 2.3.4とSWFUpload

Jetpack Flight Logから2009年10月29日の「Rails 2.3.4 and SWFUpload – Rack Middleware for Flash Uploads that Degrade Gracefully」という記事を勉強がてら訳してみした。


Rails 2.3.4とSWFUpload - Flashを使用したアップローダの互換性のあるRackミドルウェア


ウェブブラウザからのアップロードを制御はここ数年同じ方法で行ってきました。それらはスタイルするのが難しく、様々なブラウザに対して同じように表示されません。おそらく、一番、大きな問題は送信時間がどれくらいかといったフィードバックがユーザーに対してないことです。アップロードにFlashを使うという一つの方法があります。多くのライブラリがありますが、SWFUploadがお気に入りです。あなたがこの記事をよんでいるのは、おそらく、SWFUploadをRailsで利用することができないからだと思うので、FlashとRailsを一緒に使う場合の癖を取り扱い方について説明したいと思います。

Flashアップローダーを使えるようにするにはFlashの問題の為にCGIクラスを変更していました。Rails 2.3でRackがサポートされるようになって、かなり動作が変わりました。Flashからの通信を横取りするRackのミドルウェアを作成することで対処するようになります。MP3プレーヤとアップローダのちょっとしたサンプルアプリケーションを作成しました。この記事では説明しないファイルについてはサンプルをダウンロードして見て下さい。GitHub上にプロジェクトとしてこうかいしているので、クローンしてファイルを参照することもできます。

はじめに単純なSongモデルを作ってみます:

script generate model Song title:string artist:string length_in_seceonds:integer track_file_name:string track_content_type:string track_file_size:integer

title、artist、そして、length_in_secondは、アップロードされるMP3ファイルのID3タグより取得するメタデータです。他のデータはPaperclipを使って添付ファイルを処理します。また、次のようにSongモデルへいくつか簡単なバリデーションを付与します:

class Song < ActiveRecord::Base
 
  has_attached_file :track,
                    :path => ":rails_root/public/assets/:attachment/:id_partition/:id/:style/:basename.:extension",
                    :url => "/assets/:attachment/:id_partition/:id/:style/:basename.:extension"
 
  validates_presence_of :title, :artist, :length_in_seconds
  validates_attachment_presence :track
  validates_attachment_content_type :track, :content_type => [ 'application/mp3', 'application/x-mp3', 'audio/mpeg', 'audio/mp3' ]
  validates_attachment_size :track, :less_than => 20.megabytes
 
  attr_accessible :title, :artist, :length_in_seconds
 
  def convert_seconds_to_time
    total_minutes = length_in_seconds / 1.minutes
    seconds_in_last_minute = length_in_seconds - total_minutes.minutes.seconds
    "#{total_minutes}m #{seconds_in_last_minute}s"
  end
end

次にアップロード用のフォームとSWFUploader用のコンテナを次のように作ります:

- form_tag songs_path, :multipart => true do
  #swfupload_degraded_container
    %noscript= "You should have Javascript enabled for a nicer upload experience"
    = file_field_tag :Filedata
    = submit_tag "Add Song"
  #swfupload_container{ :style => "display: none" }
    %span#spanButtonPlaceholder
  #divFileProgressContainer

SWFUploader用のコンテナは、ユーザーのブラウザがFlashをサポートしていると分かるまで表示しません。いくつか問題が起こりそうな事に対して対処方法について検討してみましょう。ユーザーはFlashプレーヤーをインストールしていないかもしれませんし、インストールしていても古いバージョンかもしれません。ユーザーは、(Flashのロードに必要な)JavaScriptをインストールしていないか、有効にしていないかもしれません。Flash SWFファイルのダウンロード中に問題が発生するかもしれません。大変だぁ…幸い、swfobjectライブラリを使うことでこれらの考え得る問題について簡単に対処することができます。

ユーザーがFlashプレーヤーをインストールしていない、もしくは、古いバージョンのプレーヤーをインストールしている場合には、新しいバージョンのプレーヤーをダウンロードするリンクが含まれるダイアログが表示されます。それ以外は、通常のアップロードフォームを使うようになります。

もし、全てうまくいけば、ファンクションハンドラが互換性を保持するためのコンテナを隠して、Flashコンテナを表示します。

ああそうだ、最新のLinux用Flashプレーヤーはアップロードの進行を監視するためのイベントを発生させないので、アップロードが完了するまで、ステータスバーが表示されないことに気をつけて下さい。今のところ、これに対しての回避策はありません。

SWFUploadをJavaScript経由で初期化します。認証用のトークンやセッション情報をURL中に埋め込んでいるチュートリアルを多く見かけますが、最新のバージョンのSWFUploadでは、POSTメソッドでそれらの情報を送ることができるオプションがあり、URLに埋め込まないようにできます。

:javascript
  SWFUpload.onload = function() {
    var swf_settings = {
 
      // SWFObject settings
      minimum_flash_version: "9.0.28",
      swfupload_pre_load_handler: function() {
        $('#swfupload_degraded_container').hide();
        $('#swfupload_container').show();
      },
      swfupload_load_failed_handler: function() {
      },
 
      post_params: {
        "#{session_key_name}": "#{cookies[session_key_name]}",
        "authenticity_token": "#{form_authenticity_token}",
      },
 
      upload_url: "#{songs_path}",
      flash_url: '/flash/swfupload/swfupload.swf',
 
      file_types: "*.mp3",
      file_types_description: "mp3 Files",
      file_size_limit: "20 MB",
 
      button_placeholder_id: "spanButtonPlaceholder",
      button_width: 380,
      button_height: 32,
      button_text : 'Select Files (20 MB Max)',
      button_text_style : '.button { font-family: Helvetica, Arial, sans-serif; font-size: 24pt; } .buttonSmall { font-size: 18pt; }',
      button_text_top_padding: 0,
      button_text_left_padding: 18,
      button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT,
      button_cursor: SWFUpload.CURSOR.HAND,
      file_queue_error_handler : fileQueueError,
      file_dialog_complete_handler : fileDialogComplete,
      upload_progress_handler : uploadProgress,
      upload_error_handler : uploadError,
      upload_success_handler : uploadSuccess,
      upload_complete_handler : uploadComplete,
 
      custom_settings : {
        upload_target: "divFileProgressContainer"
      }
    }
    var swf_upload = new SWFUpload(swf_settings);
  };

上記のサンプル中の変数についてはSWFUploadの公式文書を参照して下さい。様々なイベントを取り扱うためにたくさんのハンドラがあります。このプロジェクトをクローンするなどして、それらをよく見てください。

生成されるコンテナにスタイルシートを設定する必要があります。設定方法については、サンプル中のSWFUploadのSassファイルか、Ryan Batesのnifty_generatorsファイルを見て下さい。

もう一つ注意する必要があるのは、Flashによるアップロードを扱う場合には全てのアップロードのコンテンツタイプがoctet streamになることです。mime-typesライブラリを使って、コンテンツタイプの判定を行います。ファイルの判定はファイルの拡張子のみで行われることに留意して下さい。(試したことがありませんが、mimetype-fuがファイル内のデータやマジックナンバーで判定するはずです。)デフォルトでは、SWFUploadは、ファイルパラメータの「Filedata」を呼び出します。

def create
    require 'mp3info'
 
    mp3_info = Mp3Info.new(params[:Filedata].path)
 
    song = Song.new
    song.artist = mp3_info.tag.artist
    song.title = mp3_info.tag.title
    song.length_in_seconds = mp3_info.length.to_i
 
    params[:Filedata].content_type = MIME::Types.type_for(params[:Filedata].original_filename).to_s
    song.track = params[:Filedata]
    song.save
 
    render :text => [song.artist, song.title, song.convert_seconds_to_time].join(" - ")
  rescue Mp3InfoError => e
    render :text => "File error"
  rescue Exception => e
    render :text => e.message
  end

Flashを使ったアップロードでもう一つやっかいなのはクッキーを送信しないことです。そのため、POSTデータでセッション情報を送信しています。Flashからのリクエストを傍受して、セッションキーをチェックして、キーが存在すれば、それをクッキーとしてヘッダーに挿入します。これはとても簡単なRackのミドルウェアで実現することができます。

require 'rack/utils'
 
class FlashSessionCookieMiddleware
  def initialize(app, session_key = '_session_id')
    @app = app
    @session_key = session_key
  end
 
  def call(env)
    if env['HTTP_USER_AGENT'] =~ /^(Adobe|Shockwave) Flash/
      params = ::Rack::Request.new(env).params
      env['HTTP_COOKIE'] = [ @session_key, params[@session_key] ].join('=').freeze unless params[@session_key].nil?
    end
    @app.call(env)
  end
end

Flashを使ったアップロードのチュートリアルのいくつかにあったコードを改変したものです。セッション情報は、クエリー文字列内、もしくは、POSTデータ内で有効になります。次に、このミドルウェアが実際に利用されるようにするためにconfig/initializer/session_store.rbに次の行をします:

ActionController::Dispatcher.middleware.insert_before(ActionController::Base.session_store, FlashSessionCookieMiddleware, ActionController::Base.session_options[:key])

これで完成です。サンプルプロジェクトを一度参照してみることを強くお奨めします。サンプルでは、アップロードした音楽を再生するために素晴らしいWordpress Audio Player Flashコントロールを利用しています!

0 件のコメント: