Seventytwo Post

Making SWFUpload and Rails work together

Let’s face it. HTML file uploads aren’t that great. Luckily we have tools like SWFUpload out there which allow us to make file uploading a lot better.

SWFUpload will allow us to do all sorts of fancy things including upload queues, multiple file uploads and progress info/bars.

Utilising SWFUpload with Rails can be tricky. For this tutorial we’re going to be using SWFUpload, the popular attachment_fu plugin, some attachment_fu hacks and the mimetype_fu plugin.

We will assume that you have got as far as installing attachment_fu, setting up your models and controllers and integrating SWFUpload into the process.

So what are the problems?

  1. Flash 8 doesn’t send meta data along with uploaded files which will stop Rails loading the users session, and thus, processing the uploads
  2. Rails 2 comes with Cross-Site Request Forging protection on any non-GET HTML/Ajax requests. SWFUpload uses POST requests to upload files
  3. Flash 8 sends incorrect MIME type data with uploaded files which will stop attachment_fu from recognising image uploads

1. Making SWFUpload work with Rails sessions

We’re going to need to hack the CGI::Session class in Rails so that we can pass the session ID in the query string of the Flash request and have Rails load the session correctly.

Below is the code required to make this alteration to CGI::Session courtesy of Duane Johnson.

# The following code is a work-around for the Flash 8 bug that prevents our multiple file uploader
# from sending the _session_id.  Here, we hack the Session#initialize method and force the session_id
# to load from the query string via the request uri. (Tested on Lighttpd, Mongrel, Apache)
class CGI::Session
  alias original_initialize initialize
  def initialize(request, option = {})
    session_key = option['session_key'] || '_session_id'
    query_string = if (qs = request.env_table["QUERY_STRING"]) and qs != ""
      qs
    elsif (ru = request.env_table["REQUEST_URI"][0..-1]).include?("?")
      ru[(ru.index("?") + 1)..-1]
    end
    if query_string and query_string.include?(session_key)
      option['session_id'] = query_string.scan(/#{session_key}=(.*?)(&.*?)*$/).flatten.first
    end
    original_initialize(request, option)
  end
end

Rails 2

You should put this code into a file, swfupload_session_hack.rb for example, and then put this file into the config/initializers directory of your application. Rails 2 will automatically load any files in there so this is the perfect place for it.

Older version of Rails

If you’re not yet on Rails 2 then simply append the code to the bottom of your environment.rb file.

Basically this code will look at the request URL and if it finds the session ID being passed it will load the session from this ID. The key that the code will look for in the query string will depend on how you have you app configured. It will first look for your session_key value which would normally be defined in environment.rb. For example, you can see that the session_key for the app below is _mylovelyapp_session. If it doesn’t find session_key defined in environment.rb it will instead default to looking for the _session_id key in the request URL.

The following is an example of what you might have in your environment.rb. In this case, we would need to pass the following key and value pair in the SWFUpload upload URL: _mylovelyapp_session=thesessionid.

  config.action_controller.session = {
    :session_key => '_mylovelyapp_session',
    :secret      => 'xxx'
  }

Passing the session ID in the query string is simple. All we need to do is append our key/value pair to the upload_url we have defined for our SWFUpload implementation. <%= session.session_id %> will get us the session ID. Nice.

var swfu;
$(function(){
  swfu = new SWFUpload({
    upload_url : '<%= your_upload_path %>?_mylovelyapp_session=<%= session.session_id %>',
    ...
  });
});

2. Working with Cross-site Request Forging protection in Rails 2

If you’re using anything below Rails 2 you can skip this part as CSRF protection was introduced in Rails 2.0.

Rails 2 auto-magically passes an authenticity token in any non-GET request by appending a hidden field to your forms. Because we’re not using a form for SWFUpload wer’re going to need to append the authenticy_token to the upload_url ourselves.

Fortunately this is very simple and only requires another key/value addition to the URL:

$(function(){
  swfu = new SWFUpload({
    upload_url : '<%= your_upload_path %>?_mylovelyapp_session=<%= session.session_id %>&authenticity_token=<%= form_authenticity_token %>',
    ...
  });
});

It’s easy to get the value for the authenticity token manually by using <%= form_authenticity_token %>.

Rails will look for the authenticity_token key in the request URL so you must use this as the key.

3. Hacking attachment_fu to allow content type detection of SWFUpload-ed files

The final problem we’re going to run into when using SWFUpload with Rails and attachment_fu is that SWFUpload submits files to Rails with their content type set to application/octet-stream which will prevent attachment_fu from recognising image uploads.

We’re going to use the mimetype_fu plugin to detect the mime type from our file uploads. This plugin will use the Operating System that the Rails app is running under to open the file and check on the header for the mime type. Pretty nifty. Unfortunately, for Windows users the plugin will only look at the file extension to try to determine the content type of the file.

We’ll run one of the following commands at the root of our Rails app to get it installed:

Note: There are several other ways to install plugins. E.g. Piston for SVN (and unofficially, git), Git Submodules and SVN Externals to name a few.

Add the code below into the config/initializers directory of your application. I called my file attachment_fu_hacks.rb.

Technoweenie::AttachmentFu::InstanceMethods.module_eval do
  # Overriding this method to allow content_type to be detected when
  # swfupload submits images with content_type set to 'application/octet-stream'
  def uploaded_data=(file_data)
    return nil if file_data.nil? || file_data.size == 0
    self.content_type = detect_mimetype(file_data)
    self.filename     = file_data.original_filename if respond_to?(:filename)
    if file_data.is_a?(StringIO)
      file_data.rewind
      self.temp_data = file_data.read
    else
      self.temp_path = file_data.path
    end
  end
  
  def detect_mimetype(file_data)
    if file_data.content_type.strip == "application/octet-stream"
      return File.mime_type?(file_data.original_filename)
    else
      return file_data.content_type
    end
  end
end

Attachment_fu will now check all file uploads it receives to see if their content type is ‘application/octet-stream’ and if so, will use mimetype_fu to determine the content type of the file.

Finished

And that’s all there is to it. Happy SWFUpload-ing!

Comments

  • wow… first comment.. well first do you have a download of the completed app for us to look at? also when i go to upload it comes up with a dialogue box and just has error then 422 ??? what is this?

    jflcooper 29 August 08:25

  • @jflcooper – You might want to check out Cameron Yule’s sample app on Github which is a demo Rails 2.1 app showing SWFUpload working in tandem with restful-authentication, CSRF protection and attachment_fu and is based off my article and others.

    Alistair Holt 16 September 14:19

Post a new comment

Want to add some formatting? Textile is enabled

Summary

Unfortunately Rails and SWFUpload don’t work together without a little bit of tweaking to our Rails applications. We’ll show you how to make it work.

Published on
Monday, 21 Jul 2008
Author
Alistair Holt
Navigation
Projects
Recent Posts
Feeds
Our del.icio.us

Add us to your network on del.icio.us