views:

34

answers:

2

Hello, 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

+1  A: 

I cover this in Rails 3 in Action's Chapter 8. I don't cover uploading to S3 or resizing images however.

Recommending you buy it based solely on it fixing this one problem may sound a little biased, but I can just about guarantee you that it'll answer other questions you have down the line. It has a Behaviour Driven Development approach as one of the main themes, introducing you to Rails features during the development of an application. This shows you not only how you can build an application, but also make it maintainable.

As for the resizing of images after they've been uploaded, Paperclip's got pretty good documentation on that. I'd recommend having a read and then asking another question on SO if you don't understand any of the options / methods.

And as for S3 uploading, you can do this:

has_attached_file :photo, :styles => { ... }, :storage => :s3

You'd need to configure Paperclip::Storage::S3 with your S3 details to set it up, and again Paperclip's got some pretty awesome documentation for this.

Good luck!

Ryan Bigg
+2  A: 

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.

Daniel Mendel
wow thank you... But if it "creates a new Tempfile" it's not Heroku compatible? Unless I'm missing something?
OK turns out heroku does have a place for temporary images. Other questions. Where does class RawFileStubber go?
I added the raw_file_stubber.rb file to /lib ... and then added the application.rb config setting you have above and tried to launch the server(rails s) but got hit with all kinds of errors. Q, is the above Rails 3 friendly?
@Daniel. I think I got it hooked up. I see the temp files on the harddrive but they are all TXT files. The server is erroring with "can't convert nil into String" app/controllers/photos_controller.rb:4:in `upload'lib/raw_file_stubber.rb:13:in `call'
@Daniel. The issue with this is also that it will now only work with one model. is there a general way to support this so it works for several different type of model uploads?
@user479959 You should abstract out your attachment to it's own model, like `Image` or `Attachment`, then have polymorphic associations to any models that need to use it. If you need to trigger the middleware on multiple routes / controllers, then set the path in RawFileStubber to catch `process_raw_attachment` and then it'll run before `foo/process_raw_attachment` or `bar/process_raw_attachment`, it just matches using RegExp so it could be anything as long as it's specific enough to never be a problem for other routes.
Daniel Mendel