Randomly learning Rails

thoughts aren't always a good thing…

Paperclip, valums’s file-uploader and middleware continued

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 :-)

About these ads

7 responses to “Paperclip, valums’s file-uploader and middleware continued

  1. Stanislaw February 9, 2011 at 7:04 am

    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!

  2. boddhisattvadedicates June 2, 2011 at 1:26 am

    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..

  3. Luke Saunders June 8, 2011 at 2:32 pm

    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() }
    })

  4. Luke Saunders June 8, 2011 at 2:37 pm

    Great, it ate my formatting. Here’s how it should be: https://gist.github.com/1014537

    Contact me on twitter: @lukeoflondon

  5. Armin June 17, 2011 at 9:44 pm

    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?

  6. Armin June 17, 2011 at 9:49 pm

    Answered myself :)

    Do not use instance :)

    Thank you for fantastic article again.

  7. Armin July 26, 2011 at 9:19 am

    How did you manage IE and Opera?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: