Protected File Downloads with Ruby on Rails and Paperclip

December 22, 2008 | 7 Comments |

You’re using Ruby on Rails. You want to attach files to your models. Paperclip to the rescue. It’s totally painless. Give it a go.

But how do you use it to serve protected files via your authentication method? For example, I’m currently working on a gig for a real estate site. The site lists properties. Properties have appraisals which are PDF files. The general public can see the properties but only the admins can see the attached appraisal files.

Still painless? Yes. The key is to make sure the :path for :has_attached_file points to a directory that is not publicly visible via the server and then point the :url to a controller and action that you protect. Then, in the action, use the send_file method to serve up the file.

Here’s how I’m doing it:

# app/models/appraisal.rb
class Appraisal < ActiveRecord::Base
  belongs_to :property
  has_attached_file :doc,
                    :url  => "/appraisals/:id",
                    :path => ":rails_root/assets/docs/:id/:style/:basename.:extension"
 
  attr_accessible :property_id, :doc
 
  validates_attachment_presence :doc
  validates_attachment_size :doc, :less_than => 5.megabytes
  validates_attachment_content_type :doc, :content_type => ['application/pdf']
  validates_presence_of :property_id
end
# app/controllers/appraisals_controller.rb
class AppraisalsController < ApplicationController
  before_filter :require_admin
 
  # Note: all of the other actions in this controller follow the usual REST method conventions
 
  def show
    @appraisal = Appraisal.find(params[:id])
    send_file @appraisal.doc.path, :type => @appraisal.doc_content_type, :disposition => 'attachment', :x_sendfile => true
  end
# app/views/appraisals/new.html.erb
 
<% form_for @appraisal, :html => { :multipart => true } do |f| %>
  <%= f.error_messages %>
 
<p><%= f.file_field :doc %></p>
 
  <p>
    <%= f.label :property_id, 'Attached to property:' %><br />
    <%= f.select :property_id,
		Property.find(:all).collect {|p| [ "#{p.address1} #{p.city}", p.id ] },
		{:prompt => true, :include_blank => true}
	%>
  </p>
 
  <p>
    <%= f.submit "Submit" %>
  </p>
<% end %>

I didn’t have to muck with anything else to get it to work. The only change I made outside of Rails was to add the mod_xsendfile module to Apache in order to let Apache handle the file serving. If you’re not using that and you’re copying my code, make sure you delete the :x_sendfile directive from the :show action.

I’m using Authlogic to authenticate my users and I added a simple before_filter to my Appraisal controller to keep out uninvited guests. There’s nothing stopping you from using any other type of authentication or logic to protect your files.

Here’s another handy tip. There’s no need to create an actual “assets/” directory within the root directory of your Rails app. A better idea is to create a “shared/” directory outside of your root directory and then create a symlink from “shared/assets” to “RAILS_ROOT/assets”.

Better yet, if you’re using Capistrano (especially if you’re using Capistrano), let Capistrano create the symlink for you. You can add this to your deploy recipe:

# config/deploy.rb
 
# ... the rest of your deploy stuff
 
deploy.task :symlinks do
  run "ln -nfs #{shared_path}/assets #{release_path}/assets"
end
 
after 'deploy:update_code', 'deploy:symlinks'

With the symlink in place, when Paperclip writes your files to :rails_root/assets they’ll actually be written to “shared/assets”, saving you from the hassle of having to copy all of your files from one release to the next between deployments.

Let me know if I’ve forgotten anything, if you have a better idea, or if you see a gaping security hole in my code.

Cheers!

| Tags: Ruby on Rails

7 Comments

  1. David R spake saying:

    Great post — you hit all the bases.

    One question — any problems getting xsendfile to compile on solaris? I’d tried & failed a year ago, but maybe it’s time to try again.

  2. Harry Love spake saying:

    It compiled fine for me, but I’m also using a newer (May 2008) Accelerator at Joyent. I got an error at the end of the installation when it tried to write the LoadModule directive to httpd.conf but it wasn’t too hard to add the directive manually. After that I had to add a couple attributes to my virtualhost file to turn it on for the web site. It seems to be working.

  3. akahn spake saying:

    Thanks a lot for this. The bit about the symlinks just helped me out with getting uploads to “stay” on the site after new deploys. Cheers.

  4. devian spake saying:

    How can I get a overview (thumbs) of all uploaded graphics in a gallery, if you are using paperclip for images instead of pdf-documents? The execution of send_file in combination with the rendering of a layout doesn’t work for me?

    Best regards

  5. Harry Love spake saying:

    In my example above, if I say a property has_many photos and photo is the model that I’m attaching images to (photo has_attached_file :image), then I can say

    @property.photos.each do |p|
      p.image.url
    end
  6. Richard spake saying:

    If a property has_many photos, and photo is the model that I’m attaching images to, how can I show multiple photo upload (file) form fields in the new property view, without them stepping all over each other, and what would I do in the property/photo controllers to handle that?

    Thank you for the great examples!

  7. Harry Love spake saying:

    Have a look at the series on Complex Forms at Railscasts. I think that will give you an idea of how to handle multiple file uploads.