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 attachment_fu).

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.

Andy Stewart, 08 August 2007

Posted in Rails


33 Comments

Jump to comment form
  1. 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
  2. Hi Steven, I'm delighted the article helped you out. Good luck with those callbacks!

    Andy Stewart
    13 September 2007
  3. 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
    06 November 2007
  4. 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
    07 November 2007
  5. 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
    08 November 2007
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
    07 March 2008
  11. 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
  12. 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
  13. 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
    03 April 2008
  14. Sean, thanks. That makes perfect sense and I'm embarrassed it didn't occur to me ;-)

    Andy Stewart
    07 April 2008
  15. Using this post and the comments I got swfupload + Rails 2.0.2 + cookie store + restful_authentication working. I tweaked the CGI Session hack mentioned in the post to put the 'session.session_id' 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
  16. 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
  17. Dylan, nice one -- that's terrific. Thanks for sharing your patches.

    Andy Stewart
    15 April 2008
  18. 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
  19. 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
  20. 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
  21. 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
  22. I've successfully get the session id back,but the session.data was still nil. here's some debug information: (rdb:5) session.session_id "BAh7CjoMdXNlcl9pZGkGOhFvcmlnaW5hbF91cmkwOgxjc3JmX2lkIiUxMjEz%0AYzViMzljY2NkOGIyNTJiZGYzMThmYzI3ODhiMCIKZmxhc2hJQzonQWN0aW9u%0AQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7ADoKdW50%0AYWdpBg%3D%3D--64298e780e22b47d4e333e1244a7271623d1bd49" (rdb:5) session.data {:user_id=>1, :original_uri=>nil, :csrf_id=>"1213c5b39cccd8b252bdf318fc2788b0", :untag=>1, "flash"=>{}}

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

    Genki
    01 May 2008
  23. 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
    04 May 2008
  24. 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
  25. You can replace your swf_uploaded_data 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
  26. Can you post a working example source code and all?

    Warren Noronha
    17 May 2008
  27. 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
    03 June 2008
  28. 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
    03 June 2008
  29. 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: />} 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
    08 June 2008
  30. 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
  31. Hi, does anybody have tried this using Rails 2.1?

    harm_kabisa
    09 July 2008
  32. 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
  33. In response to Parad0X, the rescue is miss-spelled. It should read:

    
     rescue ArgumentError
    

    Just a small thing.

    Harm
    11 August 2008

Have your say

You can use Markdown in your comments. If you want to post code, do this:

<pre><code class="ruby|javascript|css|html">your code here</code></pre>

Thanks!