Rails 2.3.4 and SWFUpload – Rack Middleware for Flash Uploads that Degrade Gracefully

words by Brian Racer

Browser upload controls have been pretty much the same for years. They are very difficult to style, and do not look consistent across browsers. Perhaps the biggest issue with them is they provide no feedback to the user about how long the submission will take. One alternative is to use Flash for the uploads. There are numerous libraries available, I like SWFUpload. Since the reason you are here is probably because you can’t get it working in Rails, I’m going to try and help you deal with the quirks associated with using Flash and Rails together.

It used to be you would monkeypatch the CGI class to get Flash uploaders to work due to issues with Flash. With the introduction of Rack in Rails 2.3 things now work quite differently. What we will do is create some rack middleware to intercept traffic from Flash to deal with it’s quirks. I have created a small example application of an mp3 player and uploader. You will probably want to download it, as it contains a few files not displayed in this article. You can clone it from the github project page.

First lets create a simple Song model:

./script generate model Song title:string artist:string length_in_seceonds:integer track_file_name:string track_content_type:string track_file_size:integer

title, artist, and length_in_seconds are meta-data we will pull from the ID3 tags of the uploaded mp3 file, and the rest will be used by Paperclip to handle the attachment. Lets add the paperclip attachment and a few simple validations to our new Song model:

class Song < ActiveRecord::Base
 
  has_attached_file :track,
                    :path => ":rails_root/public/assets/:attachment/:id_partition/:id/:style/:basename.:extension",
                    :url => "/assets/:attachment/:id_partition/:id/:style/:basename.:extension"
 
  validates_presence_of :title, :artist, :length_in_seconds
  validates_attachment_presence :track
  validates_attachment_content_type :track, :content_type => [ 'application/mp3', 'application/x-mp3', 'audio/mpeg', 'audio/mp3' ]
  validates_attachment_size :track, :less_than => 20.megabytes
 
  attr_accessible :title, :artist, :length_in_seconds
 
  def convert_seconds_to_time
    total_minutes = length_in_seconds / 1.minutes
    seconds_in_last_minute = length_in_seconds - total_minutes.minutes.seconds
    "#{total_minutes}m #{seconds_in_last_minute}s"
  end
end

Next comes an upload form and some containers to hold the SWFUploader:

- form_tag songs_path, :multipart => true do
  #swfupload_degraded_container
    %noscript= "You should have Javascript enabled for a nicer upload experience"
    = file_field_tag :Filedata
    = submit_tag "Add Song"
  #swfupload_container{ :style => "display: none" }
    %span#spanButtonPlaceholder
  #divFileProgressContainer

The container that holds the SWFUploader will be hidden until we know the user can support it. Initially a standard file upload form will display. A number of things can go wrong, so we need to think about a few levels of degradation here. The user might not have flash installed, the user might have an outdated version of flash, he might not have javascript installed or enabled(which is needed to load the flash), and there may be a problem downloading the flash swf file. Yikes. Luckily using the swfobject library we can easily handle all these potential issues.

If the user is missing javascript, he will see the message in the noscript tag and be presented a standard upload control.

If the user is missing flash or it is outdated, he will be presented a dialog with an upgrade link. Otherwise he can use the standard upload control.

If everything goes okey-dokey, then some function handlers we write will hide the the degradation container, and display the flash container.

Oh, and just so you know the current version of Flash Player for linux do not fire the event that monitors upload progress, so you will not get the status bar until the upload finishes. No work around for that right now.

So lets initialize the SWFUpload via some javascript. Many tutorials out there seem to put the authentication token and session information in the URL, but there are some options with current version of SWFUpload to POST and avoid that.

:javascript
  SWFUpload.onload = function() {
    var swf_settings = {
 
      // SWFObject settings
      minimum_flash_version: "9.0.28",
      swfupload_pre_load_handler: function() {
        $('#swfupload_degraded_container').hide();
        $('#swfupload_container').show();
      },
      swfupload_load_failed_handler: function() {
      },
 
      post_params: {
        "#{session_key_name}": "#{cookies[session_key_name]}",
        "authenticity_token": "#{form_authenticity_token}",
      },
 
      upload_url: "#{songs_path}",
      flash_url: '/flash/swfupload/swfupload.swf',
 
      file_types: "*.mp3",
      file_types_description: "mp3 Files",
      file_size_limit: "20 MB",
 
      button_placeholder_id: "spanButtonPlaceholder",
      button_width: 380,
      button_height: 32,
      button_text : '<span class="button">Select Files <span class="buttonSmall">(20 MB Max)</span></span>',
      button_text_style : '.button { font-family: Helvetica, Arial, sans-serif; font-size: 24pt; } .buttonSmall { font-size: 18pt; }',
      button_text_top_padding: 0,
      button_text_left_padding: 18,
      button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT,
      button_cursor: SWFUpload.CURSOR.HAND,
      file_queue_error_handler : fileQueueError,
      file_dialog_complete_handler : fileDialogComplete,
      upload_progress_handler : uploadProgress,
      upload_error_handler : uploadError,
      upload_success_handler : uploadSuccess,
      upload_complete_handler : uploadComplete,
 
      custom_settings : {
        upload_target: "divFileProgressContainer"
      }
    }
    var swf_upload = new SWFUpload(swf_settings);
  };

You will want to check out the official SWFUpload docs to understand what all of these variable do. There are many handlers we have to define to handle various events, and if you clone the project you can review them in detail.

We also need to set styles for the containers that will be generated. You can see the Sass file I created for SWFUpload here, and another one for Ryan Bates nifty_generators.

Another quirk we have to be aware of when dealing with flash uploads is that everything gets a content-type of an octet stream. We will use the mime-types library to identify it for validation. Keep in mind it only uses the extension to determine the file type. (I haven’t tested it yet, but I believe mimetype-fu will actually check file-data and magic numbers). By default SWFUpload calls the file parameter ‘Filedata’.

  def create
    require 'mp3info'
 
    mp3_info = Mp3Info.new(params[:Filedata].path)
 
    song = Song.new
    song.artist = mp3_info.tag.artist
    song.title = mp3_info.tag.title
    song.length_in_seconds = mp3_info.length.to_i
 
    params[:Filedata].content_type = MIME::Types.type_for(params[:Filedata].original_filename).to_s
    song.track = params[:Filedata]
    song.save
 
    render :text => [song.artist, song.title, song.convert_seconds_to_time].join(" - ")
  rescue Mp3InfoError => e
    render :text => "File error"
  rescue Exception => e
    render :text => e.message
  end

Another annoyance with flash uploads is that it doesn’t send cookie data. That is why we are sending the session information in the POST data. We will intercept requests from Flash, check for the session key, and if so inject it into the cookie header. We can do this with some pretty simple middleware.

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/
      params = ::Rack::Request.new(env).params
      env['HTTP_COOKIE'] = [ @session_key, params[@session_key] ].join('=').freeze unless params[@session_key].nil?
    end
    @app.call(env)
  end
end

This is a modified version from code the appears in a few tutorials about flash uploads. It will allow the session information to be in the query string *or* POST data. Next we have to make sure this middleware gets put to use so in config/initializers/session_store.rb add:

ActionController::Dispatcher.middleware.insert_before(ActionController::Base.session_store, FlashSessionCookieMiddleware, ActionController::Base.session_options[:key])

And that’s, uhh, all there is too it. Again, I really suggest you checkout the example project. It also uses the nifty WordPress Audio Player flash control to play the music you upload!


  • Pingback: Rails 2.3.4 + SWFUpload: Gracefully Degrading Rails File Uploads()

  • http://www.burninghands.net Luca Tironi

    Thanks a lot for this useful tutorial.
    I will try this method asap in my projects.

  • http://airbladesoftware.com Andy Stewart

    Having spent ages in the past getting SWFUpload and Rails to work together, I know how fiddly and time-consuming it can be. The devil is in the details.

    So I appreciate all the more your explanation of how to do it these days. The example app is very nice to have too. Excellent!

  • Alex Farrill

    Does anyone else find that this does not work with Phusion Passenger?
    Thanks,
    Alex

  • Pingback: Rails 2.3.4 + SWFUpload - Faça uploads de arquivos em Flash com estilo()

  • Mikhail

    I have plugin for uploading with Swf Upload. Works fine with rails version >= 2.3

    http://github.com/over/easy_swf_upload

  • CalebHC

    Thank you so much for this! It’s a blessing! :-)

  • Axel

    For some reason I am getting an error “Empty file” and no upload happens, although the SWFUpload reports “All files received”. It says empty file even if the app falls back to the standard HTML upload. Any suggestions on what might be wrong?

  • Pingback: Ennuyer.net » Blog Archive » Rails Reading - November 1, 2009()

  • http://jetpackweb.com Brian Racer

    @Alex Farrill – I am using it successfully with Phusion Passenger, what errors are you seeing?

  • inomaker

    I get ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
    app/middleware/flash_session_cookie_middleware.rb:14:in `call’my rails

    my rails version 2.3.4 with mod_rails

    i’m using paperclip and swfupload.

    file uploading, I get InvalidAuthenticityToken

    here is my source.

    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/
    params = ::Rack::Utils.parse_query(env[‘QUERY_STRING’])
    env[‘HTTP_COOKIE’] = [ @session_key, params[‘session_key’] ].join(‘=’).freeze unless params[‘session_key’].nil?
    end
    @app.call(env)
    end
    end

    _form.html.erb

    post_params: {”: ”,
    authenticity_token:encodeURIComponent(”)},

    session_store_rb

    ActionController::Dispatcher.middleware.insert_before(
    ActionController::Session::CookieStore,
    FlashSessionCookieMiddleware,
    ActionController::Base.session_options[:key]
    )

    what problems?

  • http://www.crowdint.com FranKaiser

    any ideas on how to make the flash player to find my file?

    ActionController::RoutingError (No route matches “/home/francisco/creative-allies/public/sonitido.mp3″ with {:canvas=>false, :method=>:get}):

  • Paul Wright

    Awesome! Thanks so much for the simple tutorial. Works great for me…..

  • http://trevorturk.com Trevor Turk

    Thanks for this. I just wanted to note that I needed to make a small change to the swfupload post_params:

    http://gist.github.com/244016

    This seemed to get everything working for me. I’m not sure why the change was necessary for me, but maybe it will help someone else.

  • wbr

    Looks promising, but uploaded files don’t show in the list.

  • Paul Harrington

    “(I haven’t tested it yet, but I believe mimetype-fu will actually check file-data and magic numbers)”

    Not that it really has anything to do with the content of your post, but right in the readme of the github link you posted the author says “I wrote mimetype_fu, a simple plugin which will try to guess the mimetype of a file based on its extension.” (A quick look at the source also confirms this). There’s no reason to give your readers false expectations if you have the information in front of you.

  • http://jetpackweb.com Brian Racer

    @Paul – I should probably update my post, however your statement is only correct for Windows users(which is noted in the README). I have taken a look at the source(lib/mimetype_fu.rb), and mimetype-fu uses the `file` utility which does in fact make use of a magic file.

  • http://philrathe.com Philippe Rathé

    Thanks Brian for that demo. I was trying the cancel features in your demo and it does not seem to work. When I clicked the red X button during a file upload, nothing happened. It works on de swfupload demo page: http://demo.swfupload.org/v220/multiinstancedemo/index.php

    Is there anything you could have added in handlers.js?

  • Mark Duncan

    Hi,

    I’m having the same File Error message Axel mentions above, i’ve dug into it a bit and it’s an empty file error. I’ve dumped the post-params and the filedata holds : Filedata”=># when i watch that temp directory i’m seeing two files created when i try to upload. RackMultipart20100120-4108-5olzvp-0 which is 0kb and a file named mongrel20100120-4108-16qghk1-0 which is 1kb bigger than the mp3 i’m uploading, when i inspect it in a hex editor i can see the params are listed at the start. Anyone have any ideas what is happening?

  • Mark Duncan

    I managed to fix the error above with a nasty hack. When an upload is done the Mongrel..1-0 file is created first and then the RackMultipart…zvp-0 is created. The Mp3info line was happening before the RackMulti file was finished writing. I’ve replaced the create action with

    def create
    require ‘mp3info’
    @timer = Time.now
    begin
    mp3_info = Mp3Info.new(params[:Filedata].path)

    logger.info(“GOT MP3 INFO”)
    song = Song.new
    song.artist = mp3_info.tag.artist
    song.title = mp3_info.tag.title
    song.length_in_seconds = mp3_info.length.to_i

    params[:Filedata].content_type = MIME::Types.type_for(params[:Filedata].original_filename).to_s
    song.track = params[:Filedata]
    logger.info(“GOT HERE”);
    song.save

    render :text => [song.artist, song.title, song.convert_seconds_to_time].join(” – “)
    rescue Mp3InfoError => e
    if @timer + 2.minutes “File error: “+e.message
    else
    retry
    end
    rescue Exception => e
    render :text => e.message
    end
    end

    it’s nasty but it does the job for now.

  • Mark Duncan

    Finally found the root cause to the above problem, on windows the HTTP_ACCEPT is sent as text/* instead of */*. This can be fixed in the middleware.

    def call(env)
    if env[‘HTTP_USER_AGENT’] =~ /^(Adobe|Shockwave) Flash/
    params = ::Rack::Request.new(env).params
    env[‘HTTP_COOKIE’] = [ @session_key, params[@session_key] ].join(‘=’).freeze unless params[@session_key].nil?
    env[‘HTTP_ACCEPT’] = ‘*/*’
    end
    @app.call(env)
    end

  • http://Railsdog David North

    I’ve got all this working great except I always get InvalidAuthenticityToken on the first attempt with a fresh browser session.

    In the log, I can see that I’m getting the authenticity token correctly in the params.
    The middleware is loaded before CookieStore and with some debug code I can see that it is adding the session data to HTTP_COOKIE e.g.

    “_myapp_session=BAh7BjoPc2Vzc2lvbl9pZCIlNDg1ZTExMzY5MGY0ZmQxYmU4MzMwNmFhNTcwNjc1ZmU=–71c3a1f402926876eab548d22bcfd03cd159162e”

    I noticed this seems to be related to the session state, if some value has been written to the session in a request previous to the one that renders my upload interface it will work. However many pages are viewed before the upload form it won’t work unless some value has been written to the session.

    Any thoughts?

  • Mark Duncan

    How are you passing the authenticity token to javascript. I was having an issue with intermittent IAT errors that were caused by the token having a space in it that was getting decoded as %20.

    This did the trick for me, don’t know if it’s related to your issue but worth a try.

    ‘scriptData’ : { ” : ”, ‘authenticity_token’ : ” },

  • Mark Duncan

    Sorry, stripped out the tags there — i’ll give it another go :)

    ‘scriptData’ : { ‘<%= get_session_key %>’ : ‘<%= u cookies[get_session_key] %>’, ‘authenticity_token’ : ‘<%= u form_authenticity_token if protect_against_forgery? %>’ },

  • David North

    I can see from the logs that the token is correct, otherwise it wouldn’t work on subsequent requests. Same goes for the session data.
    On the first request, my session doesn’t contain the key “_csrf_token”, that seems to be responsible for the session mis-match that results in the IAT error.

    When I write something to the session, on the next request I can see that _csrf_token exists and the session data is longer. Then the upload will work.

  • David North

    I think this is due to the session data I’m sending being out of date. Although I’m initializing the session by writing a value to it on the request that displays my upload form, the contents of cookies[session_key] reflect what was loaded from the cookies, its not until the next request that this value is correct (including the csrf token). Somehow I need to get a value for the session data that reflects the current state of the session so it’ll match with the authenticity token in the upload request.

  • Pingback: File uploads in Ruby on Rails | Space Babies()

  • http://blog.programmanstalt.de Jan Foeh

    For anyone having the same problem David had, here’s what I did: being sleep deprived and unable to come up with an elegant solution, I resorted to an ugly hack in application_controller.rb.

    before_filter :fix_missing_session, :only => :edit

    private

    def fix_missing_session
    if request.get? && cookies[ActionController::Base.session_options[:key]].blank?
    session[:the_answer] = 42
    redirect_to request.headers[“REQUEST_URI”] and return
    end
    end

    Whenever one of the edit pages with SWFupload in them is loaded without the session cookie being set, I set the session and reload the page. A better solution would probably be to encode a session string manually, or to calculate a new csrf token matching the empty session…

  • Xujhxxffuh

    I’m slightly confused where the app knows to take each uploaded file and create a new record? I don’t see any loops or anything.

  • http://jetpackweb.com Brian Racer

    SWFUpload is making a separate POST request for each file that you add to it.

  • kill me now please

    HAML? ARE YOU FUCKING SERIOUS? REMOVE THE BULLSHIT HAML

  • Pingback: How can I do a large file upload using Sinatra, haml, nginx, and passenger? - Admins Goodies()

  • Pingback: Log Racks: Not Just For Those North of the Snow Line | Log Racks Deal Grabber()

  • Pingback: AS3, Flex, Flash, PureMVC, Games » Upload snapshot image from flash to server()

  • Pingback: Flash Games Download» Blog Archive » Upload snapshot image from flash to server()

  • Simar

    Thank you for your valuable information.Today I found your blog and I read you post, it is really good..
    http://www.songs.we.bs/sadda-adda-songs-free-mp3-download.html