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

Aug 21, 2012

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](http://github.com/Valums-File-Uploader/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: ```language-html <div id="my_file_uploader"></div> ``` ```language-js 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](http://stackoverflow.com/questions/7203304/warning-cant-verify-csrf-token-authenticity-rails/8798687). 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: ```language-ruby # 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.