Sorry this page looks weird. It was automatically migrated from my old blog, which had a different layout and different CSS.

Uploading Files With SWFUpload

Update: this can work with cookie-based sessions: see Dylan Vaughn’s comments below.

Another update: this gets better and better! Nathan Colgate has put together a screencast showing how to hook up Rails 2.0.2, attachment_fu, SWFUpload and RESTful authentication. See his comments below.

It’s About The Tea

Sometimes it’s useful to be able to upload lots of files to your webapp in one go. Rather than sitting at your computer uploading one photo after another, just queue them all up and wander off for a nice cup of tea instead.

SWFUpload is a Flash app that lets you have more nice cups of tea. It also lets you filter file types and sizes, displays progress bars and is fully styleable with CSS.

Getting SWFUpload working in your Rails app isn’t rocket surgery. But it would have been quicker for me if the various tips scattered around the web had all been in one place. So here they are.

The Warm Up

Before you start tinkering with SWFUpload, get normal HTML file uploads working. This proves that your server-side code works and lets your app degrade gracefully should the user have Flash or Javascript disabled.

Configuration Over Convention

SWFUpload is a Flash/JavaScript library and isn’t tailored to Rails. There are many configuration options and it isn’t immediately clear which of them are necessary — especially to those used to convention over configuration.

Happily the gallic Flornet has a minimal demo Rails app with SWFUpload you can download and pull apart. Poking around with this helped me quickly work out what was important and what wasn’t.

Getting With The Session

Unfortunately, with out-of-the-box SWFUpload, Rails can’t pick up the user’s session. Flash 8 doesn’t send meta data with the uploaded files so Rails doesn’t know which session to load. There’s also no way to add a hidden field to the multipart form data.

The solution comes in two parts. This won’t work as is with cookie-based sessions, Rails 2’s default session store — but it will work with Dylan Vaughn’s modifications (see comments).

First hack Ruby’s CGI::Session class like this.

Second, append your app’s session key and value to the upload_script argument in the SWFUpload constructor. Mine looks like this:

upload_script: "<%= assets_path @client %>?_photocms_session_id=<%= session.session_id %>"

It’s About The Callbacks

Maybe it’s just me but I didn’t grasp at first the significance of SWFUpload’s callback architecture and ability to upload multiple files. In my Rails action I wanted to redirect to a new page once all the files were uploaded and I couldn’t understand why SWFUpload was complaining about the 302s.

Eventually the penny dropped. SWFUpload calls your action once per file, not once for all the files, and so the action must simply return a 200 status code. Anything else will interfere with SWFUpload before it’s finished. Here’s how my action looks now:

def create
  # HTML file upload
  if params[:asset]
    @asset = @client.assets.create! params[:asset]
    flash[:notice] = 'Successfully uploaded asset.'
    redirect_to client_path(@client)
  # SWFUpload file upload
  elsif params[:Filedata]
    @asset = @client.assets.create! :swf_uploaded_data => params[:Filedata]
    render :nothing => true
  end
end

So the way to react to uploaded files is to code up the uploadFileComplete(file) and uploadQueueComplete(file) JavaScript callbacks.

Around the time my brain cell got into gear, Peter De Berdt kindly explained it all in detail.

Missing MIME Type

Unfortunately Flash 8 sends malformed MIME type data to the server. This means that the content type is always set to application/octet-stream which in turn means attachment_fu, for example, won’t resize images (because it doesn’t know they are images).

My answer was to use the MIME::Types gem to deduce them. Install it like this:

$ sudo gem install mime-types

I then added an swf\uploaded\data= method to my model, based on attachment\_fu’s uploaded\_data= method. Here it is:

def swf_uploaded_data=(file_data)
  return nil if file_data.nil? || file_data.size == 0 
  # Map file extensions to mime types.  Thanks to bug in Flash 8
  # the content type is always set to application/octet-stream.
  self.filename = file_data.original_filename if respond_to?(:filename)
  mime = MIME::Types.type_for(self.filename)[0]
  self.content_type = mime.blank? ? file_data.content_type : mime.content_type
  if file_data.is_a?(StringIO)
    file_data.rewind
    self.temp_data = file_data.read
  else
    self.temp_path = file_data.path
  end
end

Voilà — the content type sorts itself out.

Plugins That Make This Easier

Having hacked my way this far I suddenly started finding various plugins that do most of this for you. I’m sure they weren’t there when I looked the first time. I haven’t tried any of them but you may wish to:

For attachment\fu and MIME types: mimetype\_fu (see Thomas’s comment which pushes the MIME type deduction into attachmentfu).

For SWFUpload and attachment_fu: ActiveUpload.

Conclusion

SWFUpload is a really nice way of improving the file upload experience for users. Flash 8 has a few hitches but none is intractable. And if it all goes pear-shaped, you can fall back to plain old HTML — which isn’t so bad anyway.

Comments



Hi Andy,
Very cool article here mate, kudos in your general direction.
I used both SWFUpload and Attachement_fu independently, however my current project called for a combo.
Thanks to this article and some tinkering its working well though some further tinkering is need to get the callbacks working.
Cheers

Steven

Steven Holloway • 13 September 2007



Hi Steven, I’m delighted the article helped you out. Good luck with those callbacks!

Andy Stewart • 13 September 2007



Hi Andy.

This is the second time I’ve used this information, so I thought it only right to show you some kudos for you work.

Thanks a lot. You’ve saved me hours of hassle and possibly a minor stroke :)

Keep up the good work.

Jim Neath • 6 November 2007



I wonder if it isn’t a security issue to pass the session id through a query string? Couldn’t I upload files to someone else’s session by sending a request with a tweaked query string?

Andrew Levitt • 7 November 2007



Andrew, that’s a good point. However I think Rails 2’s CSRF killer would protect the application from requests forged with somebody else’s session id – if the anti-forgery token were passed along by SWFUpload (in the query string?).

This rests on the assumption that although a compromised page may be able to get your session id, it would not be able to generate the correct token without knowing the server secret.

However I haven’t tried any of this so I’m not certain….

Andy Stewart • 8 November 2007



Hey, I’m experiencing session problems with swfupload but only in production mode… when running in development mode on localhost I don’t get any problems at all! any idea why this would be happening?

Jonzo • 17 December 2007



Jonzo, do you mean production mode on your server or production mode on localhost? If on your server, perhaps the problems stem from a different web server configuration or different version of Rails from localhost’s. If on localhost, I’m not sure.

Andy Stewart • 18 December 2007



Jonzo, I’ve looked at this in more detail and found the problem is caused by Rails 2’s cookie-based sessions which default to accepting the session id only from the cookie.

You can turn this off for the whole application by including the following in your app’s configuration:

ActionController::Base.session_options[:cookie_only] = false

Better yet, you can turn it off only for specific actions inside a controller like this:

class YourController < ApplicationController
  session :cookie_only => false, :only => :create
end

So now your app will allow the session ID to come from any source. However I still can’t get the session management to use the session ID passed in the query string. Hmm.

In the meantime you can bypass the problem by using a different session store.

Andy Stewart • 18 December 2007



Hi thanks Andy!

I re-read my question and it seems confusing now… I should clarify my problem just in case anyone else hits it maybe…

Just before I start: I was having this problem before I implemented Duane Johnson’s solution.

After implementing that solution everything worked fine :-) so nothing you said in your blog post was wrong, I’m just curious about the erratic behaviour that caused me to find this post in the first place!

Anyway, like I said in my earlier comment, swfupload worked when uploading to localhost, but when I deployed my code to the staging server it stopped working because of the session problem. It didn’t have anything to do with the environment because it still worked fine in production mode on localhost, both computers are running rails 1.2.3.

Yeah! just curious really, but everything is workin ;-)

On an unrelated note…

One thing I didn’t find clear in your post was that I didn’t know the name to give my session_id in my url. in your example you say:

Mine looks like this: upload_script: “<%= assets_path @client %>?_photocms_session_id=<%= session.session_id %>”

What I didn’t realise is that you probably defined your session id in application.rb like so:

session :session_key => '_photocms_session_id'

so I ended up lookin around the web for a while until I found out how to do that, and then I remembered declaring it 3 months ago when I started this project! So yeah, just a little reminder to anyone out there having the same ?!? moment as I did when your session_id wasn’t being passed through to rails.

Jonzo • 18 December 2007



I appreciate your article and placing blame where it belongs :)

Most of the issues remaining in SWFUpload are actually issues in the Flash Player (i.e. bad mime-types).

Jake Roberts • 7 March 2008



It is a cool toy but you will not be able to service all the browser clients out there.

Macs in general will give 302 header errors, you will lose session state in some browsers etc etc

Maybe in next couple of versions of both (flash and swfupload), things will be better. As of right now I am cautious. Great for personal sites but would not put it together for a big client.

Thanks

Scott • 18 March 2008



Scott, I have to say that next time round, I’ll be choosing plain old HTML file upload over SWFUpload. SWFUpload looks good but just seems to cause problems. HTML file upload may be boring, but it works reliably.

Upload progress bars do give useful feedback, but you don’t need Flash for a progress bar. Having said that, the alternatives are still clunky, thanks to HTTP’s not anticipating the need for client-side callbacks.

Andy Stewart • 18 March 2008



Re Andy’s comment:
“the problem is caused by Rails 2’s cookie-based sessions”

From what I gather, it is impossible to use rails cookie_store sessions and swf_upload together. This is because cookie store means session data is actually being stored on the users machine. Sending a session_id to rails is useless because the session data is not on the server.

This should be a caveat in your post when you are describing the session hack:

“The solution comes in two parts. First hack Ruby’s CGI::Session class like this.”

Sean Corbett • 3 April 2008



Sean, thanks. That makes perfect sense and I’m embarrassed it didn’t occur to me ;-)

Andy Stewart • 7 April 2008



Using this post and the comments I got swfupload + Rails 2.0.2 + cookie store + restfulauthentication working. I tweaked the CGI Session hack mentioned in the post to put the ‘session.sessionid’ into options[‘session_data’], then I monkey patched some parts of the cookie store to pull out that data if it exists.

Check it out

I also had to put:

code class="ruby">session :cookie_only => false, :only => :create

into my controller (my upload action is ‘create’)

Dylan Vaughn • 11 April 2008



Oops, typo on my last comment, the thing to put into your controller is:

session :cookie_only => false, :only => :create

Dylan Vaughn • 11 April 2008



Dylan, nice one – that’s terrific. Thanks for sharing your patches.

Andy Stewart • 15 April 2008



An important note to add to Dylan’s code is that you can not pass the session through SWFUploads post-param. Dylan’s (great) hack only scrubs the URI, not the params.

Pastie

Nathan Colgate • 15 April 2008



I thought I hit the jackpot when I got to the end of your post and saw ActiveUpload… since I’ve been having so many issues hacking it all together… BUT I guess it doesn’t work on Rails 2 :( I shall wait…

Dianna • 16 April 2008



Dianna,

I’m using Dylan’s hack + attachment_fu + s3+ restful_authentication + SWFUpload on Rails 2.0.1 (and 2.0.2) without ActiveUpload. ActiveUpload didn’t seem very extensible.

Screencast

Good luck!

Nathan Colgate • 16 April 2008



Nathan, good work! The screencast helps enormously in understanding how you got everything working. Thanks for taking the time to put it together.

Andy Stewart • 17 April 2008



I’ve successfully get the session id back,but the session.data was still nil.
here’s some debug information:
(rdb:5) session.sessionid
“BAh7CjoMdXNlcl9pZGkGOhFvcmlnaW5hbF91cmkwOgxjc3JmX2lkIiUxMjEz%0AYzViMzljY2NkOGIyNTJiZGYzMThmYzI3ODhiMCIKZmxhc2hJQzonQWN0aW9u%0AQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7ADoKdW50%0AYWdpBg%3D%3D–64298e780e22b47d4e333e1244a7271623d1bd49”
(rdb:5) session.data
{:user
id=>1, :originaluri=>nil, :csrfid=>“1213c5b39cccd8b252bdf318fc2788b0”, :untag=>1, “flash”=>{}}

(rdb:26) session.data {“flash”=>{}} (rdb:26) session.session_id “BAh7CjoMdXNlcl9pZGkGOhFvcmlnaW5hbF91cmkwOgxjc3JmX2lkIiUxMjEz%0AYzViMzljY2NkOGIyNTJiZGYzMThmYzI3ODhiMCIKZmxhc2hJQzonQWN0aW9u%0AQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7ADoKdW50%0AYWdpBg%3D%3D–64298e780e22b47d4e333e1244a7271623d1bd49”

Genki • 1 May 2008



Now I can pick up the session,but it’s kind of a “read-only” session.I can get the data from the session,but can not write anything in it.
Here’s the scenario,I upload several pictures,and after redirecting to another page,i want to tag them.So I was trying to save the photo.id in the session[:untagged] so that I can get the pictures back on the next page.Any idea?

genki • 4 May 2008



Thank you so much. This post pools together so many great resources that actually work. Everything else out there on the web is out of date or incomplete. Thanks!!

Brian Moschel • 12 May 2008



You can replace your swfuploadeddata method with this:

def swf_uploaded_data=(data)
  data.content_type = MIME::Types.type_for(data.original_filename)
  self.uploaded_data = data
end

Also be sure to do require ‘mime/types’ somewhere.

winton • 15 May 2008



Can you post a working example source code and all?

Warren Noronha • 17 May 2008



It seems that with Rails 2.1, the session string is separated into multiple lines by a newline character (perhaps to make for cleaner logs?). This causes a javascript error when inserted into the ‘upload url =’ call in the view (unterminated string literal). Once you scrub out those newlines it works fine:

_your_session_id=<%= session.session_id.gsub("\\n","")

Sean O'Hara • 3 June 2008



Okay, nix my above comment, or at least the gsub method called on the session_id string. It now causes the following error: CGI::Session::CookieStore::TamperedWithCookie, at least with more recent versions of mongrel. So I have now changed the code to this:

_your_session_id=<%= CGI::escape(session.session_id)

Sean O'Hara • 3 June 2008



Hi !

I am very grateful for the information compiled within this website, thanks a lot for the god work. However i am having some problems, maybe one of the readers of the author might help, i have installed and used the demo by dave south over at apped design (where swfupload is used). I have added your session handling (within the url while uploading with swfupload). I the log i can see that the session_id is indeed there,

Processing PhotosController#swfupload (for 192.168.1.50 at 2008-06-07 10:34:50) [POST] Session ID: d5fc02eb1b335d40db5f33963079f328 Parameters: {“Filename”=>“malte.jpg”, “session_id”=>“d5fc02eb1b335d40db5f33963079f328”, “action”=>“swfupload”, “Upload”=>“Submit Query”, “controller”=>“photos”, “Filedata”=>#<File:/tmp/CGI1234-1>} Photo Create (0.000684) INSERT INTO photos (“content_type”, “size”, “thumbnail”, “updated_at”, “session_id”, “filename”, “height”, “parent_id”, “created_at”, “width”) VALUES(‘image/jpeg’, 1354944, NULL, ‘2008-06-07 10:34:54’, NULL, ‘malte.jpg’, 600, NULL, ‘2008-06-07 10:34:54’, 800) Photo Load (0.000775) SELECT * FROM photos WHERE (photos.“thumbnail” = ‘thumb’ AND photos.“parent_id” = 223) LIMIT 1

But as seen the session_id is not inserted into the database table.

If any of you out there has a hint please let me know.

Sinc Kalle

Kalle Johansson • 8 June 2008



All we need session for in this action is user_id, right?

before_filter :login_required, :except => :create

skip_before_filter :verify_authenticity_token, :only => :create

def create
    # Restore session settings
    _options = {}
    ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.each { |key, value| _options[ key.to_s ] = value }
    Rails.configuration.action_controller.session.each { |key, value| _options[ key.to_s ] = value }
    _options['new_session'] = false
    _options['session_id'] = params[ Rails.configuration.action_controller.session[:session_key].to_sym ]
    _options['no_cookies'] = true

    # Start session
    begin
      session = CGI::Session.new(request, _options)
      current_user = User.find( session[:user_id] )
    rescue ArgumentException
      redirect login_url
      return
    end
...
end

Parad0X • 11 June 2008



Hi, does anybody have tried this using Rails 2.1?

harm_kabisa • 9 July 2008



Ok, none of those bits of code were working for me on Rails 2.1. Dylan’s original didn’t actually get a query string somehow.

This pastie (combining dylan’s and code I found elsewhere) works for me on Rails 2.1 (mongrel): http://pastie.org/243653

I did have to encode the session_id, as Sean mentioned above – you can use the u() helper, <%= u(session.session_id) %>.

David • 29 July 2008



In response to Parad0X, the rescue is miss-spelled. It should read:

 rescue ArgumentError

Just a small thing.

Harm • 11 August 2008



Found a solution for Rails, but anybody knows how to deal with the cookie bug in Django?

omtv • 3 November 2008



This is very valuable information, so kudos to you!

Roob • 8 January 2009



Great article! Thanks mate.

Freehill Media Website Design • 9 April 2009



The monkey patch of Dylan Vaughn for making this work with the cookie store breaks in rails 2.3. Found a nice rack-based approach in the following blog post, which is not likely to break in future rails versions.

http://thewebfellas.com/blog/2008/12/22/flash-uploaders-rails-cookie-based-sessions-and-csrf-rack-middleware-to-the-rescue

Ewout • 7 June 2009



Thx for the link, Ewout, will try that! been looking for solutions for 5 hours now … including:

authlogic documentation.

did anyone EVER got swfupload to work with authlogic and rails 2.3?

Monkey • 23 August 2009



Ok boys and girls, after 10 hours of fiddling in the dark I got it running with rails 2.3.2, attachment_fu, swfupload and omygolly: authlogic :-) short before the finish line I ran into

WARNING: Can’t mass-assign these protected attributes: swf_uploaded_data

which I countered with

attr_accessible :swf_uploaded_data

in my attachment.rb, which would be your asset.rb I guess …

(not to forget the “require ‘mime/types’” on top of that, I really ran into everything one could possible do wrong … oh my)

Frank the Tank • 26 August 2009



Hey, guys, did anyone namage to make authlogic and swfupload live together? Please replyand share your solution cause I have already spent too much time to solve the issue, but nothing at the end.

Thanks in advance

Roody • 14 October 2009

Andrew Stewart • 8 August 2007 • Rails
You can reach me by email or on Twitter.