Randomly learning Rails
thoughts aren't always a good thing…
Paperclip, valums’s file-uploader and middleware continued
Posted by on December 3, 2010
So I thought I’d share how I got this nice mixture working. The first thing I did was contemplate what method I should use to intercept the incoming data. I could’ve put it all in the controller, however, I felt this would be bad use of the controller. Likewise I could’ve put it in the model, however, in the end I decided upon writing it as rack middleware and injecting it into the application.
First I created the view. I won’t go into detail, but basically it looks something like this:
<!-- contents above file-uploader -->
<div id="file-uploader">
<noscript>
<p>Please enable JavaScript to use file uploader.</p>
<!-- or put a simple form for upload here -->
</noscript>
</div>
<!-- contents below -->
I then implemented the JavaScript and put it in a seperate file I called upload.js – I load this file via a contents_for block in my view. The JavaScript requires jQuery, however thats easy enough to change. Make sure you change the action path to the path to your controller.
$(function() {
function createUploader(){
var uploader = new qq.FileUploader({
element: $('#file-uploader')[0],
action: '/photos',
multiple: false,
allowedExtensions: ['jpg', 'jpeg', 'png']
});
}
createUploader();
});
Then in my controller, PhotosController, I have:
class PhotosController < ApplicationController
def create
@photo = current_user.photos.build(params[:photo])
if @photo.save
render :text => '{"success": true}', :content_type => "application/json"
end
end
end
In my model – I have a basic Paperclip setup:
class Photo < ActiveRecord::Base
has_attached_file :image,
:styles => {
:large => "800x600>",
:medium => "400x300>"
}
end
Below is the code where the “magic” happens. I can’t claim credit for the code, as it’s largely inspired by the links you’ll see in the comments. I called the file raw_file_upload.rb, however, feel free to call it what you wish (as long as you rename the class as well). All it does is check if the incoming request is an Ajax request and whether certain conditions are met before sending it to the convert_and_call method. In the convert_and_call method it firstly creates a temporary file thats used to grab the input and store it on the server until it is processed by Paperclip. It then recreates the input so Rails is fooled into thinking this is a regular form. Notice in the multidimensional hash, I name it “image” (which corrosponds to the has_attachment :image, above). This way my controller logic requires very little differentiating between a regular upload request and a AJAX request. Afterwords it sets the environment variables and calls the application.
require 'mime/types'
#
# Built with massive inspiration (ok, more like following their lead) from the following:
# - https://github.com/JackDanger/file-uploader/commit/03ed9ba68d46805e22a0014ac0eee9ecbd5acd8d
# - https://github.com/newbamboo/example-html5-upload/blob/master/lib/rack/raw_upload.rb
#
class RawFileUpload
def initialize(app)
@app = app
end
def call(env)
if raw_file_post?(env)
convert_and_call(env)
else
@app.call(env)
end
end
private
def raw_file_post?(env)
env['HTTP_X_FILE_NAME'] &&
env['CONTENT_TYPE'] == 'application/octet-stream' &&
env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
end
def convert_and_call(env)
tempfile = Tempfile.new('raw-upload.')
env['rack.input'].each do |chunk|
tempfile << chunk.force_encoding('UTF-8')
end
tempfile.flush
tempfile.rewind
multipart_hash = {
:image => {
:filename => env['HTTP_X_FILE_NAME'],
:type => MIME::Types.type_for(env['HTTP_X_FILE_NAME']).first,
: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']['photo'] = multipart_hash
env['rack.request.query_hash']['photo'] = multipart_hash
@app.call(env)
end
end
This code is setup as rack middleware, basically I created an initializer file in config/initializers/ and called it raw_upload_helper.rb. This file initializes the above file and inserts it as rack middleware.
Rails.application.config.middleware.use RawFileUpload
That should be it. Good luck, and comment if you have a problem
Helped me a lot. Great Thanks. I wondered by absense of comments.
It works only for 1.9.2.
Is there any possibility to make Your code work with ruby 1.8.7?
I already tried it (force_encode method downgrade needed). But the result is that files uploaded are only parts of their originals (jpg uploaded simply broken). I can’t figure what change is required.
Thanks again!
Hello Moortens,
Thank you very much for the blog post..This is just what I needed. I am just facing one roadblock, where I would be grateful to seek your guidance.. I am using Rails 2.0.2 and I can’t exactly configure the middle ware how you have done it in your app wrt Rack.. I am also a newbie.. Thus I really don’t know how can I call my raw_file_upload.rb in my case..to do the “magic”..Can you please direct me on how can I get this working for me.. . I am using this version of Rails for project specific purpose..
This was very useful and it works well. The one thing I had to add was the auth token to get around the Rails forgery protection. To do this I added this function to my layout template:
function authToken(){
return ”;
}
And then add the authenticity_token param to the FileUploader options:
new qq.FileUploader({
…
params: { authenticity_token: authToken() }
})
Great, it ate my formatting. Here’s how it should be: https://gist.github.com/1014537
Contact me on twitter: @lukeoflondon
Ola. Thank you for this. Works great in most cases.
However, there is a problem when using instance in model (asset.rb) like this:
has_attached_file :data,
:styles => lambda { |attachement|
attachement.instance.content_type =~ /^video/ ? video_styles : pdf_styles
},
:processors => lambda { |attachement|
attachement.instance.content_type =~ /^video/ ? video_processors : pdf_processors
}
I get:
undefined method `instance’ for #
Any ideas?
Answered myself
Do not use instance
Thank you for fantastic article again.
How did you manage IE and Opera?