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.