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?
- Flash 8 doesn’t send meta data along with uploaded files which will stop Rails loading the users session, and thus, processing the uploads
- Rails 2 comes with Cross-Site Request Forging protection on any non-GET HTML/Ajax requests. SWFUpload uses POST requests to upload files
- 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)
alias original_initialize initialize
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(/=(.*?)(&.*?)*$/).flatten.first
end
original_initialize(request, option)
end
endRails 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.
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:
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:
- Git:
script/plugin install git://github.com/mattetti/mimetype-fu.git - Subversion:
script/plugin install http://mimetype-fu.googlecode.com/svn/trunk/
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'
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
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
endAttachment_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!
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