Ernie Miller

No, I don't work in NYC, DC, or the valley, and I'm cool with that.

RSS Feed

Uploadify and Rails 3

Posted by Ernie on July 9, 2010 at 7:37 am

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.

Filed under Blog
Tagged as ,
You can leave a comment, or trackback from your own site.
  • smoku

    Great! It really helped me :)

  • dxg

    You seem to have a typo:
    config.autoload_paths
    Should be:
    config.load_paths

    Also, I went slightly further with the middleware and added this:

    env['rack.request.query_hash'].delete @session_key
    env['rack.request.form_hash'].delete @session_key

    so that the session data doesn’t get sent to the controller.

  • http://thebalance.metautonomo.us Ernie

    Not a typo — load_paths became autoload_paths not too long ago.

  • http://blog.darkhax.com/ Daniel Huckstep

    While this was very helpful and solved my problems, I had already promised my first born son to the Flash gods to try to get this shit to work.

    People need to quit whining about which video codecs HTML5 should support and add a AJAX file uploads.

  • http://www.railsdog.com David North

    In your version of the middleware, the addition of ::Rack::Utils.escape breaks it for me – % symbols get encoded such that env['HTTP_COOKIE'] doesn’t match what I see in regular requests e.g. “Q%3D” becomes “Q%253D”. This is with Rails 3.0.0.rc

  • http://thebalance.metautonomo.us Ernie

    David,

    Not sure what to say to that one. I am using edge rails, but the code I have above is being used in an application right now and working fine.

  • http://thebalance.metautonomo.us Ernie

    Daniel,

    I agree, using Flash for this sort of thing really chafes me. Actually, Flash in general chafes me.

  • Pingback: Using Uploadify with Rails 3 | glrzad

  • Pingback: Uploadify onComplete Wouldn’t Fire Until I Swore – A Lot | Nobody Listens Anyway

  • http://michaelvanrooijen.com/ Michael van Rooijen

    Hey!

    I managed to get everything working and (wait for it…..) it doesn’t work in Internet Explorer. Now, I don’t mean that Uploadify doesn’t work but upon trying to log in to my application, it dies due to: “ActionController::InvalidAuthenticityToken”

    I am using Devise for authentication, so maybe there is some kind of conflict or issue with Rack/Middleware? If you have any clue why this happens during login in Internet Explorer (8), I don’t have other IE’s. Then please let me know. : )

    Great guide!

  • http://michaelvanrooijen.com/ Michael van Rooijen

    After some digging and hacking around I came up with a solution. I will post it here for anyone that might come across this issue.

    http://gist.github.com/626860

    Works for me with Devise, Uploadify and Rails 3.0.0 (not beta/rc)

  • http://thebalance.metautonomo.us Ernie

    Michael,

    Glad you got it figured out. I’m unsure why you ended up with the trouble, since I’m using it on an app that integrates with Devise and we have users running IE as well (unfortunately). That being said, I’ll keep an eye out for breakage and try your version if it comes up (plus my js.erb hack with _http_accept).

  • Sadiksha

    I tried using it in my app in rails 3, but I am getting the error that says:
    NoMethodError (undefined method `join’ for #):
    app/middleware/flash_session_cookie_middleware.rb:14:in `call’

    any idea about what i should do?

  • Pingback: Trevor Turk — Links for 10-21-10

  • Brett

    Thanks for this pot. Still working to get this up and running. Starting from scratch, I’m juggling between this post, and the other.

    The other post mentions adding

    “@asset.file_content_type = MIME::Types.type_for(@asset.original_filename).to_s”

    but it doesn’t say where, and I don’t see how to customize that per app. Any thoughts?

  • hollownest

    Rails 3 Flash session handling can all be done at once with this gem:

    https://github.com/trevorturk/flash_cookie_session

  • http://peachyweb.com Ben

    Thanks a lot for your help, this is much appreciated!

  • brewster

    so this is all working, however i am trying to on each uploadify JS request, save the file data to a session temporarily so that i can later process them with another form. for whatever reason, i cannot save session data from my controller on the uploadify JS requests, but it does work with a normal HTML request. any thoughts?

  • Ryan

    I have made a demo here: https://github.com/rdetert/FormFly

    There is a strange problem I’m having though. In the before_filter of the posts_controller.rb, the session variable disappears (and then reappears) between requests about 10% of the time.

    The reason I’m saving the uploads is in case the user has to reload the page for some reason.

    Maybe someone can help diagnose the problem or help me think of a better way to approach the problem.

  • Ryan

    Ended up getting things working but had to use a workaround with ActiveRecord. Still not sure what the problem was.

  • http://www.abraaocoelho.com/ Abraão Coelho

    it’s been a while since this post was published but maaan, thanks! it certainly helped a lot (after a while banging my head against the wall).

  • Rob

    Make sure you double-encode it  

    encodeURI(encodeURIComponent(‘#{cookies[key]}’))

  • http://avishaiweiss.net Avishai

    I’ve gotten this to sort of work…. The upload succeeds (you can go to the photos page and see the newly uploaded photo), except Rails still returns Completed 406 Not Acceptable. Any ideas?

About

I'm Ernie Miller. But then, you probably knew that by looking at the page title, or the URL. I'm a Ruby programmer in Louisville, Kentucky. This blog used to be called "metautonomo.us", which I thought was kind of clever, but nobody, including me, could type it. Lesson learned.