July 21, 2009

Posted by John

Tagged harmony, paperclip, and video

Older: Code Review: Weary

Newer: Getting Started With MongoMapper and Rails

Uploadify and Rails 2.3

A few weeks back we (Steve and I) added multiple asset upload to Harmony using Uploadify. If you are thinking that sounds easy, you would be sorely mistaken. Uploadify uses flash to send the files to Rails. This isn’t a big deal except that we are using cookie sessions on Harmony and flash wasn’t sending the session information with the files, so to Rails the files appeared as unauthenticated.

We found multiple articles online showing how to get this working, but none of them worked as promised. At the time Harmony was running on Rails 2.2. Knowing that rack was probably the best way to solve our issue, we updated to 2.3, which was pretty painless, and started hacking. Be sure to check out a quick screencast of the finished product at some point as well.

Add Uploadify

First, we added the uploadify files and the following js to the assets/index view. We actually set many more options, but these are the ones pertinent to this article. Script is the url to post the files to. fileDataName is the name of the file field you would like to use. scriptData is any additional data you would like to post to the url.

<%- session_key_name = ActionController::Base.session_options[:key] -%>
<script type="text/javascript">
  $('#upload_files').fileUpload({  
      script          : '/admin/assets',
      fileDataName    : 'asset[file]',
      scriptData      : {
        '<%= session_key_name %>' : '<%= u cookies[session_key_name] %>',
        'authenticity_token'  : '<%= u form_authenticity_token if protect_against_forgery? %>'
      }
  });
</script>

As you can see, it adds the session key and the cookie value along with the authenticity token as data that gets sent with the file. We then use a piece of rack middleware to intercept the upload and properly set the Rails session cookie.

Add Some Middleware

We created an app/middleware directory and added it to the load path in environment.rb.

%w(observers sweepers mailers middleware).each do |dir|
  config.load_paths << "#{RAILS_ROOT}/app/#{dir}"
end

Next, we dropped flash_session_cookie_middleware.rb in the app/middleware directory.

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'])
      
      unless params[@session_key].nil?
        env['HTTP_COOKIE'] = "#{@session_key}=#{params[@session_key]}".freeze
      end
    end
    
    @app.call(env)
  end
end

And, finally, we added the following to our session_store.rb initializer.

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

This inserts our middleware before ActionController’s CookieStore so that everything will just work as expected.

Assign the Content Type

The only other thing we needed to do was manually set the content type of the file. We were using paperclip (which is awesome) to do uploads, so something like this did the trick:

@asset.file_content_type = MIME::Types.type_for(@asset.original_filename).to_s

Be sure to add the mime type gem to your environment.rb file as well.

config.gem 'mime-types', :lib => 'mime/types'

But Why?

So why did we go through all this trouble to allow multiple uploads at once? Taking a quick look at the finished product might help. I didn’t record the entire screen in the video, as we haven’t actually released Harmony yet (ooooh secrets!), but I did capture enough that you can see the awesome uploads in action.

Harmony Multi-Uploading of Assets

Hope this spares some other poor soul attempting the same thing some time.

19 Comments

  1. Finally a simple, awesome solution to what is a hard problem. No more trying to crowbar upload.swf into applications.

  2. Hi John. I’ve been trying to shoehorn uploadify into our app the last couple days (without modifying uploadify source). In our app we have an Asset model that uses paperclip to store asset records. Uploadify POSTs the upload to the create action in our assets controller which creates a new asset record, but additional form fields are not sent in that POST. Without JS/Flash all form attributes are of course posted. We’ve resorted to redirecting to an edit form (in JS) following the ajax uploadify POST of a single upload to complete the additional asset model attributes. (1) Would you mind sharing some of your model and controller code (here or in the uploadify forum)? The Rails/REST examples are lacking. I’m also planning a blog post and test app for the scenario I’m describing, email me if you want to collaborate.

  3. @Andy – We are just doing files and not currently allowing changing titles and such. My model and controller does nothing special. Haven’t needed to do what you are doing yet. If I do I’ll be sure to post.

  4. In practice I have found Uploadify difficult (impossible in its current form?) to work with in this regard. Things I’ve tried (1) auto:false uploadify option, and do a jquery $.post being sure to send extra form fields, (2) use scriptData to grab form data with jquery and post it, (3) create a Asset record and have the form post to the update action by passing '_method':'put' in scriptData and other means, but I can’t trick rails into going to the update action. As I mentioned we have a 2-step process now, which works for a single or multiple uploads. Second step for multiple uploads would be editing a batch of assets attributes (titles/descriptions for example), Flickr comes to mind as an example of this approach. If you add the ability for users to customize the names or descriptions of their uploads with Harmony, would love to see some of your implementation!

  5. This has been tremendously helpful. However, I seem to have a slight issue that I can’t figure out. Below I have pasted my “scriptData” portion for uploadify as it appears in the source code on the rendered page. I assume that the “u” in the code must be url-encoding these strings, so “+” becomes “%2B” or “=” becomes “%3D.”

    
    'scriptData' : {
       '_DHDA_session' : 'BAh7CiIVdXNlcl9jcmVkZW50....',
       'authenticity_token' : "R4%2BOZiOjf8XoHDzkPM1rY7yKb%2B1QUnM6XUPfS7GU2r0%3D"
    }
    
    

    My problem is that when uploading a file, my log shows

    
    ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
      app/middleware/flash_session_cookie_middleware.rb:18:in `call'
    

    and the params show

    
    "authenticity_token"=&gt;"R4 OZiOjf8XoHDzkPM1rY7yKb 1QUnM6XUPfS7GU2r0="
    

    Hmmm, why did the + signs turn into spaces? The = sign seemed to convert just fine. Any ideas?

    Dave

  6. Smeevil Smeevil

    Jul 25, 2009

    I seem to run into the exact same problem as Dave does…. been trying all kinds of things to get the + sign encoded/decoded correctly but without luck so far…

  7. @Dave and @Smeevil – That is actually Rack::Utils.parse_query. I remember hitting that at some point and having issues, but then I did something differently and the problems went away.

    Can’t remember what. I can promise that my final solution is what I showed above and that it is in fact working for me. If anything hits me, I’ll let you know.

  8. smeevil smeevil

    Jul 25, 2009

    @John Well the encoding is solved now using :

    =='authenticity_token'  : encodeURIComponent('#{u form_authenticity_token if protect_against_forgery?}'),

    But alas still getting the InvalidAuthenticityToken :/
    The post looks good though :

    Parameters: {"Filename"=>"test.png", “gallery_session”=>"66c523f937f33432a4e660906dfc0e14", “folder”=>"/smeevil/albums/7/photos/", “authenticity_token”=>"3R0ccSdAVZD3X5hWOTfzoaMvrHwRHMrS6WDy+BgWBk4=", “album_id”=>"7", “photos”=>{"content"=>#<File:/var/folders/9c/9cYOHQdXHyuCFvOglxcd7k+++TQ/-Tmp-/RackMultipart.7710.1>}, “Upload”=>"Submit Query", “userid”=>"1"}

    Still investigating , if I find something I’ll let you know :)

  9. smeevil smeevil

    Jul 25, 2009

    Found it !
    Somehow when i added the session_key_name to the script variable it worked…

    ==‘script’ : ‘/photos/?#{session_key_name}=#{u cookies[session_key_name]}’,

  10. John, thanks for the fantastic write up. You published this tuesday. Friday uploadify 2.0.0 was released. And I needed this stuff… today. Couldn’t have been better.

    Here are a few things that I needed to get mine working (please note, I’m using Uploadify 2.0.0):

    • The addition of the ‘method’ parameter set to ‘get’. Otherwise the middleware was not getting anything in the env[‘QUERY_STRING’]
    • The use of smeevil’s encodeURIComponent around the authenticity_token. Otherwise the plus signs were getting stripped out and I was getting the very frustrating InvalidAuthenticityToken error.

    I’m not sure what the session_key_name thread means, other than adding it to the script parameter might be a way to get around using the method parameter.

    Anyway, my updated view:

    
    $(document).ready(function() {
      $('#fileInput').uploadify({
        'script' : assets_path,
        'method' : 'get',
        'fileDataName'  : 'asset[file]',
        'scriptData' : {
          '&lt;%= session_key_name %&gt;' : '&lt;%= u cookies[session_key_name] %&gt;',
          'authenticity_token' : encodeURIComponent('&lt;%= u form_authenticity_token if protect_against_forgery? %&gt;')
        }
      });
    });
    

    Thanks again.

  11. Excellent! Yes, I forgot to mention that I, too, was using the new Uploadify 2.0. However, I did try the 1.6.2 release with no luck either. Also, I had tried using the encodeURIComponent as well, but without the “u” method – this didn’t work. But with the “u” – it does work. Can someone explain why you need the “u” there too? From what I can tell, the u simply url encodes the string. Isn’t that the same thing that encodeURIComponent does?

    Lastly, I did post to the uploadify forum about this problem. One of the developers replied saying that something has changed in how scriptData deals with variables – more info here
    http://www.uploadify.com/forum/viewtopic.php?f=7&t=1446

  12. Does this work with polymorphic assets?

  13. Setting the method to GET for a file upload didn’t quite make sense to me. Rather, I used the Rack::Request object instead of env[‘QUERY_STRING’], which got me working. So call in the middleware now looks like:

      def call(env)
        if env['HTTP_USER_AGENT'] =~ /^(Adobe|Shockwave) Flash/
          
          req = Rack::Request.new(env)
          unless req.params[@session_key].nil?
            env['HTTP_COOKIE'] = "#{@session_key}=#{req.params[@session_key]}".freeze
          end
        end
    
        @app.call(env)
      end

    I also had to double-wrap the scriptData that’s passed to uploadify, like so:

    
    scriptData  : {
       '&lt;%= session_key_name %&gt;' : encodeURIComponent('&lt;%= u cookies[session_key_name] %&gt;'),
       'authenticity_token'  : encodeURIComponent('&lt;%= u form_authenticity_token if protect_against_forgery? %&gt;')
    }
    

    Hope that helps out a bit!

    —riney

  14. james2m james2m

    Jul 30, 2009

    I don’t get it, isn’t Uploadify a just a version of SWFUpload with the js converted to jQuery?

  15. There’s been a healthy thread in the comments going on about Middleware and Flash based uploaders since December last year on The Web Fellas blog

    Looks like it has a slightly different variant on the middleware but is the version used in this swf upload and rails sample on github

    Finally, Jim Neath made a few blog posts around flash uploaders that are also worth a read.

  16. Spent several hours trying to get multiple asset records created with uploadify and paperclip. Always get an error in paperclip creating the second record. Put together a sample app that illustrates the problem with full details. Would anyone be able to lend a hand? Would love the help!

    uploadifytest on github

  17. env[‘QUERY_STRING’] is empty for me(rails 2.3.3) so I used next:

    
      def call(env)
        if env['HTTP_USER_AGENT'] =~ /^(Adobe|Shockwave) Flash/
          params = Rack::Request.new(env)
    
          unless params[@session_key].nil?
            env['HTTP_COOKIE'] = "#{@session_key}=#{params[@session_key]}".freeze
          end
        end
    

    @app.call(env)
    end

  18. Issue was with Rails 2.3.3! Rails 2.3.2 works great. Issue filed for Paperclip.

  19. Hello. I’ve been trying to follow this and other similar tutorials for Rails, paperclip, and Uploadify. It’s been tough, but I think I’m getting close. I managed to get past the authenticity woes, but now am getting other errors, such as:

    NoMethodError (undefined method ‘original_filename’
    or
    You have a nil object…

    my code is taken mostly from your example.

    In my controller:

    
      def create
        @image = Image.new(params[:image])
        @image.photo_content_type = MIME::Types.type_for(@image.original_filename).to_s
        respond_to do |format|
          if @image.save
            flash[:notice] = 'Asset was successfully created.'
            format.html { redirect_to(@image) }
            format.xml  { render :xml =&gt; @image, :status =&gt; :created, :location =&gt; @image }
          else
            format.html { render :action =&gt; "new" }
            format.xml  { render :xml =&gt; @image.errors, :status =&gt; :unprocessable_entity }
          end
        end
      end
    

    I don’t seem to have any issues with token authenticity, so I’ll leave that code out. But here’s the javascript:

    
    &lt;%- session_key_name = ActionController::Base.session_options[:key] -%&gt;

    $(document).ready(function() {
    $(‘#upload_input’).uploadify({
    ‘uploader’: ‘/flash/uploadify.swf’,
    ‘cancelImg’: ‘/images/cancel.png’,
    ‘script’ : ‘/images’,
    ‘scriptData’ : {
    ‘<%= session_key_name >’ : ‘<= u cookies[session_key_name] >’,
    ‘authenticity_token’ : ‘<
    = u form_authenticity_token if protect_against_forgery? %>’
    }
    });
    });


    I’ve tried mixing code up from different, similar tutorials, but end up making it worse, or just getting myself confused.

    Rails is actually saving the image object because I have db records showing only the created_at and updated_at times. All other fields, the paperclip ones(i.e. photo_content_type, photo_file_name, etc.) are all NULL. Any help would be appreciated. Thanks.

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.