Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails - Paper_Clip - Support for Multi File Uploads

I have paper_clip installed on my Rails 3 app, and can upload a file - wow that was fun and easy!

Challenge now is, allowing a user to upload multiple objects. Whether it be clicking select fileS and being able to select more than one. Or clicking a more button and getting another file upload button.

I can't find any tutorials or gems to support this out of the box. Shocking I know...

Any suggestions or solutions. Seems like a common need?

Thanks

like image 289
user479959 Avatar asked Oct 22 '10 22:10

user479959


2 Answers

Okay, this is a complex one but it is doable. Here's how I got it to work.

On the client side I used http://github.com/valums/file-uploader, a javascript library which allows multiple file uploads with progress-bar and drag-and-drop support. It's well supported, highly configurable and the basic implementation is simple:

In the view:

<div id='file-uploader'><noscript><p>Please Enable JavaScript to use the file uploader</p></noscript></div>

In the js:

var uploader = new qq.FileUploader({
   element: $('#file-uploader')[0],
   action: 'files/upload',
   onComplete: function(id, fileName, responseJSON){
     // callback
   }
});

When handed files, FileUploader posts them to the server as an XHR request where the POST body is the raw file data while the headers and filename are passed in the URL string (this is the only way to upload a file asyncronously via javascript).

This is where it gets complicated, since Paperclip has no idea what to do with these raw requests, you have to catch and convert them back to standard files (preferably before they hit your Rails app), so that Paperclip can work it's magic. This is done with some Rack Middleware which creates a new Tempfile (remember: Heroku is read only):

# Embarrassing note: This code was adapted from an example I found somewhere online
# if you recoginize any of it please let me know so I pass credit.
module Rack
  class RawFileStubber

    def initialize(app, path=/files\/upload/) # change for your route, careful.
      @app, @path = app, path
    end

    def call(env)
      if env["PATH_INFO"] =~ @path
        convert_and_pass_on(env)
      end
      @app.call(env)
    end

    def convert_and_pass_on(env)
      tempfile = env['rack.input'].to_tempfile      
      fake_file = {
        :filename => env['HTTP_X_FILE_NAME'],
        :type => content_type(env['HTTP_X_FILE_NAME']),
        :tempfile => tempfile
      }
      env['rack.request.form_input'] = env['rack.input']
      env['rack.request.form_hash'] ||= {}
      env['rack.request.query_hash'] ||= {}
      env['rack.request.form_hash']['file'] = fake_file
      env['rack.request.query_hash']['file'] = fake_file
      if query_params = env['HTTP_X_QUERY_PARAMS']
        require 'json'
        params = JSON.parse(query_params)
        env['rack.request.form_hash'].merge!(params)
        env['rack.request.query_hash'].merge!(params)
      end
    end

    def content_type(filename)
      case type = (filename.to_s.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
      when %r"jp(e|g|eg)"            then "image/jpeg"
      when %r"tiff?"                 then "image/tiff"
      when %r"png", "gif", "bmp"     then "image/#{type}"
      when "txt"                     then "text/plain"
      when %r"html?"                 then "text/html"
      when "js"                      then "application/js"
      when "csv", "xml", "css"       then "text/#{type}"
      else 'application/octet-stream'
      end
    end
  end
end

Later, in application.rb:

config.middleware.use 'Rack::RawFileStubber'

Then in the controller:

  def upload
    @foo = modelWithPaperclip.create({ :img => params[:file] })
  end

This works reliably, though it can be a slow process when uploading a lot of files simultaneously.

DISCLAIMER

This was implemented for a project with a single, known & trusted back-end user. It almost certainly has some serious performance implications for a high traffic Heroku app and I have not fire tested it for security. That said, it definitely works.

like image 58
Daniel Mendel Avatar answered Nov 01 '22 03:11

Daniel Mendel


The method Ryan Bigg recommends is here:

  • https://github.com/rails3book/ticketee/commit/cd8b466e2ee86733e9b26c6c9015d4b811d88169
  • https://github.com/rails3book/ticketee/commit/982ddf6241a78a9e6547e16af29086627d9e72d2

The file-uploader recommendation by Daniel Mendel is really great. It's a seriously awesome user experience, like Gmail drag-and-drop uploads. Someone wrote a blog post about how to wire it up with a rails app using the rack-raw-upload middleware, if you're interested in an up-to-date middleware component.

  • http://pogodan.com/blog/2011/03/28/rails-html5-drag-drop-multi-file-upload
  • https://github.com/newbamboo/rack-raw-upload
  • http://marc-bowes.com/2011/08/17/drag-n-drop-upload.html

There's also another plugin that's been updated more recently which may be useful

  • jQuery-File-Upload
  • Rails setup instructions
  • Rails setup instructions for multiples

And another one (Included for completeness. I haven't investigated this one.)

  • PlUpload
  • plupload-rails3

These questions are highly related

  • Drag-and-drop file upload in Google Chrome/Chromium and Safari?
  • jQuery Upload Progress and AJAX file upload
like image 30
6 revs Avatar answered Nov 01 '22 04:11

6 revs