Uploadify and Rails 3

Comments

If you're using Uploadify, the nifty jQuery file upload plugin -- which we'll forgive for providing all its examples in PHP (*shudder*) -- there's a good chance you followed the general pattern outlined by John Nunemaker over at RailsTips. His article describes in detail how to get Flash to play nicely with Rails sessions using Rack middleware. Unfortunately, Rails 3 requires a few minor changes to this setup.

In your view

First up, as before, you'll need to tell Uploadify about the name of your application's session key. The important change here is where the session key name can now be found: Rails.application.config.session_options[:key].

    <% session_key_name = Rails.application.config.session_options[:key] %>
      $('#<%= "#{asset_name}_upload" %>').uploadify({
          uploader        : '/uploadify/uploadify.swf',
          script          : '<%= url_for @project %>',
          fileDataName    : 'project[<%= "#{asset_name}_attributes" %>][data]',
          cancelImg       : '/uploadify/cancel.png',
          fileDesc        : 'Photo or Video',
          fileExt         : '*.mov;*.mp4;*.avi;*.wmv;*.png;*.jpg;*.gif',
          auto            : true,
          sizeLimit       : <%= 100.megabytes %>,
          width           : 150,
          height          : 25,
          hideButton      : true,
          wmode           : 'transparent',
          buttonText      : 'Upload',
          onComplete      : function(a, b, c, response){ eval(response); $('#<%= "#{asset_name}_queue" %>').html(''); },
          queueID         : 'noQueueForMePlease',
          onProgress      : function(event, queueID, fileObj, data){ $('#<%= "#{asset_name}_queue" %>').html(data.percentage + '%') },
          scriptData      : {
            '_http_accept': 'application/javascript',
            '_method': 'put',
            '<%= session_key_name %>' : encodeURIComponent('<%= u cookies[session_key_name] %>'),
            'authenticity_token': encodeURIComponent('<%= u form_authenticity_token %>')
          }
      });

A brief aside about my application

The preceding snippet was taken from an actual application which is a work-in-progress. This application has several file upload "slots" that are handled by accepts_nested_attributes_for on my Project model, and because the request is always an "update" of the parent Project model, it's safe for me to hard-code the _method as put. That may not be the case for you. You will want to examine the following parameters from this example and change them as needed for your application's requirements:

  • script: the URL you want Uploadify to post to. Remember that this can differ depending on whether you want to create or update a record. Thankfully, url_for takes this into account when generating a URL from an AR object by checking whether it's been persisted or not.
  • fileDataName: this should be the same as the file field name generated for your file attribute by the FormBuilder file_field helper.
  • fileDesc and FileExt: modify to suit the file extensions you would like to accept. You should still validate at the model level, but this will prevent the file select dialog from allowing selection of other file types.
  • sizeLimit: this one should be fairly obvious.
  • hideButton and wmode: leave these out if you want the default button to be visible. I'm replacing it with some text placed behind the (invisible) Flash button.
  • _http_accept in scriptData and onComplete: I'm opting to send along a request for a .js.erb by passing _http_accept, and eval the response in onComplete in much the same way as jQuery does when you pass 'script' to the dataType parameter of jQuery.ajax.
  • queueID and onProgress: Uploadify will create a queue with a progress bar below the button by default. I don't like this behavior as the queue is too large for the slide-out drawer I'm using for files. I pass a bogus queueID and set up the onProgress callback to instead replace a percentage number in a div of my choosing.
  • _method in scriptData: Since I'm always posting an update to an existing project (because i'm using accepts_nested_attributes_for) I can safely hardcode put here. If you may be creating a new record, you will want to set this conditionally to post, based the result of @myobject.persisted?

The middleware

Now, you can see we're passing the session key and form authenticity token in the above code. The trick is that the session key is stored in a cookie, and Flash doesn't know about it. Enter a slightly modified version of John's FlashSessionMiddleware.

app/middleware/flash_session_cookie_middleware.rb:

    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/
          req = Rack::Request.new(env)
          env['HTTP_COOKIE'] = [ @session_key,
                                 req.params[@session_key] ]
                               .join('=').freeze unless req.params[@session_key].nil?
          env['HTTP_ACCEPT'] = "#{req.params['_http_accept']}"
                               .freeze unless req.params['_http_accept'].nil?
        end

        @app.call(env)
      end
    end

Note that aside from setting the HTTP_COOKIE header to include the session, we're also setting the HTTP_ACCEPT header (to allow our Rails app to serve up the .js.erb in a respond_to block). Place this file somewhere handy (I followed his suggestion of app/middleware) and make sure it's loaded at Rails startup by adding the following to config/application.rb inside the class Application block :

        # Add additional load paths for your own custom dirs
        %w(observers mailers middleware).each do |dir|
          config.autoload_paths << "#{config.root}/app/#{dir}"
        end

Inserting our middleware

This was the trickiest part. A lot has changed about the Rails boot process between 2.3 and 3.0. While searching for help on getting this set up, I found a bunch of information, most of it bad, on what would work, from different locations for the code to different code altogether. In the end, as it turns out, you can still place the middleware loading code in config/initializers/session_store.rb, but it's changed quite a bit from Rails 2.3:

    Rails.application.config.middleware.insert_before(
      ActionDispatch::Session::CookieStore,
      FlashSessionCookieMiddleware,
      Rails.application.config.session_options[:key]
    )

That's it! Hope this helps you avoid the hassles I dealt with in getting Uploadify up and running with Rails 3.

comments powered by Disqus