Drag-n-Drop upload that works with RoR and Paperclip

Aug 21, 2012

UPDATE 2018! Paperclip is deprecated

I recently wanted to implement a drag and drop browser upload to one of my existing Rails applications. Even though it was not difficult, it felt quite rewarding once it was working (because I don't like the classic upload forms). So I decided to share the solution for anyone who wants to do something similar.

Note, I am not gonna go through the basics of how Paperclip works or how you should configure it for your needs, I assume that you already have Paperclip working or know how to get it to work.

Clientside: Valum's File Uploader

I looked around for a client side solution and I stumbled upon Valum's File Uploader which did pretty much what I wanted it to. The necessary files are fileuploader.js and fileuploader.css and can be found in the client directory at their github repository. So I grabbed those files and added them to my assets.

Then you can initiate the fileuploader widget like this:

 <div id="my_file_uploader"></div>
var uploader = new qq.FileUploader({
    element: document.getElementById('my_file_uploader'),
    action: '<%= add_files_post_path(@post) %>',
    params: { "authenticity_token": "<%= form_authenticity_token %>" },
    customHeaders: { "X-File-Upload": "true" }
});

The element parameter is of course the DOM node where the widget will be initiated at and action is the path to your Rails controller action which will process the upload. The path in my example is a custom route that I will show later on.

Then I use the params parameter to set the authenticity token. Without it, the XHR request would fail unless you have removed protect_from_forgery from your application_controller.rb but if you have done that then... well you shouldn't. There are multiple ways to include the token to your request and I only choose this way to make it simple. If you want to look at other options then take a look at this question at StackOverflow.

Finally I add a customHeader to the request called X-File-Upload and set it to "true". I will explain this further in the Server side section.

For further questions regarding the fileuploader configurations or styling etc, please check out their documentation at github.

Serverside: Get Paperclip to handle the upload

Here is a summary of the relevant parts of my Rails configuration:

routes.rb

resources :posts do
  post :add_files, :on => :member
end



post.rb

class Post < ActiveRecord::Base
  has_many :post_files
  ....
end



post_file.rb

class PostFile < ActiveRecord::Base
  belongs_to :post
  has_attached_file :attachment, paperclip_configurations...
  ....
end



posts_controller.rb

class PostsController < ApplicationController
  before_filter :parse_raw_upload, :only => :add_files
  ...

  def add_files
    @post_file = @post.post_files.build(attachment: @raw_file)
    if @post_file.save
      render js: { success: true }
    else
      render js: { success: false }
    end
  end

private
   def parse_raw_upload
    if env['HTTP_X_FILE_UPLOAD'] == 'true'
      @raw_file = env['rack.input']
      @raw_file.class.class_eval { attr_accessor :original_filename, :content_type }
      @raw_file.original_filename = env['HTTP_X_FILE_NAME']
      @raw_file.content_type = env['HTTP_X_MIME_TYPE']
    end
  end

end

I think some of this is kind of self explanatory. PostsController which handles the Post model. Post model has many PostFile models. PostFile model holds the paperclip attachment.

The part that connects the dots with the fileuploader widget is what happens in the parse_raw_upload method. Because when files are uploaded using the fileuploader, it will be sent using application/octet-stream and that means that the file will not appear in the params hash in your controller. Instead it will be available through the syntax env['rack.input'].

But lets take it one step at a time. In the fileuploader configuration I added the custom header X-File-Upload. That was done as a verification that now there is a file on the way that is not in the params. So the first thing that happens in parse_raw_upload is to check if there is anything to parse. It might seem unnecessary but it is how I did it.

The next thing that happens is that when an upload is happening then the file is assigned to the instance variable @raw_file. This object is of the type StringIO and here is the next problem. Usually with paperclip, you can take the file and send it to the attachment= instance method that will parse the file and retrieve the meta data from it. But a StringIO class does not respond to either original_filename nor content_type. And that is why we are doing a class_eval on the StringIO, to create accessor methods that paperclip will use. And fileuploader is sending the values for those in the headers X-File-Name and X-Mime-Type.

The last thing to mention is that the fileuploader determines success on wether it gets a json object back with success set to true or not. That is what happens in my add_files controller action.

Good luck :)

Fell free to leave feedback or mention if something is not working.