Pat Shaughnessy

Ribadesella, Spain

Paperclip sample app part 2: downloading files through a controller

May 16, 2009 · 17 comments

Last time I wrote about how to quickly setup a Rails application using scaffolding that allows users to upload image files and then display them using the Paperclip plugin. Paperclip does the simplest thing possible by default: it saves the file attachments right on the file system of your web server, allowing you to download them to users easily using Apache or whatever web server you have installed.

Today I’d like to take that sample app one step further and show how to use a Rails controller to download the files, instead of directly through Apache. To get the finished code just go to http://github.com/patshaughnessy/paperclip-sample-app and look at the “part2” folder.

There are a variety of reasons why you might want to do this, including:

  • Security: you don’t want to expose all of the file attachments to all users of your web site. Instead, you want to implement some business rules and show some files to some users, but not to others.
  • Auditing/logging: you want to keep track of who is viewing which files, or how many times they are viewing them.
  • You want to hide the actual location of the files from users, and instead map the files to URLs in some other pattern or manner.
  • Or, you might want not want to store the files on your web server’s file system at all, but instead in a database table or somewhere else. In Part 3 of this series, I’ll show how to do this…

The common thread here is that you want to execute some Ruby code every time a users accesses a file, and the way to do that is by routing the download requests through a controller.

Let’s pick up where we left off last time:

If we take a look at some of the HTML source for this page:

<h1>Listing users</h1>
<table>
  <tr>
    <th>Photo</th>
    <th>Name</th>
    <th>Email</th>
  </tr>
<tr>
    <td><img alt="Mickey-mouse"
      src="/system/avatars/1/thumb/mickey-mouse.jpg?1242395876" /></td>
    <td>Mickey Mouse</td>
    <td>mickey@disney.com</td>
    <td><a href="/users/1">Show</a></td>
    <td><a href="/users/1/edit">Edit</a></td>

… we can see that Paperclip’s “url” function which we called in index.html.erb is returning a pointer to the actual location of the file on the web server’s hard drive, under the public/system folder:

$ find public/system
public/system
public/system/avatars
public/system/avatars/1
public/system/avatars/1/original
public/system/avatars/1/original/mickey-mouse.jpg
public/system/avatars/1/small
public/system/avatars/1/small/mickey-mouse.jpg
public/system/avatars/1/thumb
public/system/avatars/1/thumb/mickey-mouse.jpg

Now let’s say that we want to implement some simple security around these images…. reason #1 from my list above. The first thing we’ll need to do, then, is to remove the image files from the public folder and instead save them in some non-public place on our web server, for example:

$ mkdir non-public
$ mv public/system non-public/.

Now let’s double check that Apache can’t find the files in their new location:

Great! We see a missing image as expected. No users can see the files unless we run some bit of Ruby code to enable access. Now… how can we use a controller to download the files through Rails, instead of through Apache? The first thing we need to do is add a route to routes.rb for accessing the files.

If you open up routes.rb and look at what the scaffolding generator created for us, you’ll see this:

ActionController::Routing::Routes.draw do |map|
  map.resources :users
  ...etc...
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

The map.resources line indicates that our application supports a series of routes that handle the four REST actions: GET, POST, PUT and DELETE. The best way to get a handle on how the routes work is by running “rake routes” to list out all the URL patterns that Rails will match on:

$ rake routes
(in /Users/pat/rails-apps/paperclip-sample-app)
    users GET    /users(.:format)          {:action=>"index", :controller=>"users"}
          POST   /users(.:format)          {:action=>"create", :controller=>"users"}
 new_user GET    /users/new(.:format)      {:action=>"new", :controller=>"users"}
edit_user GET    /users/:id/edit(.:format) {:action=>"edit", :controller=>"users"}
     user GET    /users/:id(.:format)      {:action=>"show", :controller=>"users"}
          PUT    /users/:id(.:format)      {:action=>"update", :controller=>"users"}
          DELETE /users/:id(.:format)      {:action=>"destroy", :controller=>"users"}
                 /:controller/:action/:id           
                 /:controller/:action/:id(.:format)

The last two lines are the “default routes,” which connect any URL matching the pattern controller/action/id to the corresponding controller. We could just go ahead and use the default routes, but to learn a bit more about how map.resources works, let’s create a new URL pattern for our users that we’ll use in a minute to download the avatar file attachments with. Edit routes.rb and add the :member parameter in bold:

ActionController::Routing::Routes.draw do |map|
  map.resources :users, :member => { :avatars => :get }
etc...

If we now re-run rake routes, we can see that a new URL pattern was created for us that we can use to download the avatar images via a GET request:

$ rake routes
(in /Users/pat/rails-apps/paperclip-sample-app)
      users GET    /users(.:format)          {:action=>"index", :controller=>"users"}
            POST   /users(.:format)          {:action=>"create", :controller=>"users"}
   new_user GET    /users/new(.:format)      {:action=>"new", :controller=>"users"}
  edit_user GET    /users/:id/edit(.:format) {:action=>"edit", :controller=>"users"}
avatars_user GET   /users/:id/avatars(.:format) {
                                              :action=>"avatars",
                                              :controller=>"users"
                                             }
       user GET    /users/:id(.:format)      {:action=>"show", :controller=>"users"}
            PUT    /users/:id(.:format)      {:action=>"update", :controller=>"users"}
            DELETE /users/:id(.:format)      {:action=>"destroy", :controller=>"users"}
                   /:controller/:action/:id           
                   /:controller/:action/:id(.:format)

Now to get the avatar for user 7, for example, we can issue a URL like this:

http://localhost:3000/users/7/avatars

… and the request will be routed to the “avatars” action in the “users” controller (plural since a user might have more than one style of avatar). So now let’s go right ahead and implement the avatars method and add some code to download a file to the client. The way to do that is to use ActionController::Streaming::send_file. It’s simple enough; we just need to pass the file’s path to send_file as well as the MIME content type which the client uses as a clue for deciding how to display the file, and that’s it! Let’s hard code these values for now and see if it all works (update the path here for your machine):

class UsersController < ApplicationController
  def avatars
    send_file '/path/to/non-public/system/avatars/1/original/mickey-mouse.jpg',
      :type => 'image/jpeg'
  end

Now if you type http://localhost:3000/users/1/avatars into your browser you should see the mickey image again. If not, then double check your code changes, where the files are actually located on the hard drive now and also try stopping and reloading the Rails app since changes to routes.rb are cached and are only loaded when Rails is initialized.

Instead of hard coding the path in the avatars method, we obviously need to be able to handle requests for any avatar file attachment for any user record. Before we enhance our code to do this, let’s take a few minutes to configure Paperclip and tell it where the files are now stored on the file system, and which URL we have configured our routes.rb file to use. This will make our work coding in the controller a lot easier, and also indicate to Paperclip where new file attachments should be uploaded to. To do this, we need to add a couple of parameters to our call to has_attached_file in our User model (user.rb), shown here in bold (again, update the path for your machine):

class User < ActiveRecord::Base
  has_attached_file :avatar,
    :styles => { :thumb => "75x75>", :small => "150x150>" },
    :path => '/path/to/non-public/system/avatars/1/original/mickey-mouse.jpg',
    :url => 'users/1/avatars'
end

Just to take one step at a time, I’ve hard coded the URL and path again here in the model. But now we can generalize our code in UserController to handle any user, like this:

def avatars
  user = User.find(params[:id])
  send_file user.avatar.path, :type => user.avatar_content_type
end

Now we can test http://localhost:3000/users/1/avatars again to be sure that we haven’t broken anything. If it’s all working, lets’ proceed to clean up the hard coding in user.rb. It turns out that Paperclip uses the same interpolations idea that we saw above in routes.rb. So I can use symbols like :rails_root, :id, :style, etc., and they will be evaluated to the values I expect and need. Here’s the finished code in my model:

has_attached_file :avatar,
  :styles => { :thumb => "75x75>", :small => "150x150>" },
  :path => 
    ':rails_root/non-public/system/:attachment/:id/:style/:basename.:extension',
  :url => '/:class/:id/:attachment'                    

If we open up the console and take a look at our user object there, we can see that Paperclip is substituting the correct values for each of the symbols I’ve provided:

$ ./script/console
Loading development environment (Rails 2.3.2)
>> User.first.avatar.url
=> "/users/1/avatars?1242395876"       (time stamp appended here automatically)
>> User.first.avatar.path
=> "/Users/pat/.../non-public/system/avatars/1/original/mickey-mouse.jpg"

Now let’s go back and test our original application again with our new code:

  • The new route in routes.rb
  • The new action in UserController
  • And the :url and :path parameters added to has_attached_file in the User model.

Oops… we see the large image again. It doesn’t work! What happened? Well, it turns our that we forgot one detail in our new avatars method in UserController. Our code always returns the default, or “original” style of the file attachment, but in the users index view we actually display the thumbnail image using image_tag user.avatar.url(:thumb). So our controller code needs to be able to handle requests for other styles as well. To do this, we need to pass the requested style somehow. The simplest thing to do is just to add the style as a URL parameter to the download request, like this:

  http://localhost:3000/users/1/avatar?style=thumb

(We could also add the style to the URL's path, but that would require another change to routes.rb.) To make this work, first I need to tell Paperclip about how I want to handle the style value in the URL:

has_attached_file :avatar,
  :styles => { :thumb => "75x75>", :small => "150x150>" },
  :path => 
    ':rails_root/non-public/system/:attachment/:id/:style/:basename.:extension',
  :url => '/:class/:id/avatar?style=:style'

And now I need to handle this new parameter in my controller:

def avatars
  user = User.find(params[:id])
  style = params[:style] ? params[:style] : 'original'
  send_file user.avatar.path(style),
            :type => user.avatar_content_type
end

I don’t need to change anything in index.html.erb since there we call image_tag user.avatar.url(:thumb) which picks up the new URL pattern from Paperclip.

And now, if I’ve got all of this correct, I should be able to finally see the thumbnail image again on the index page:

And finally we have files being downloaded by Ruby code present in the UserController class. If we actually wanted to implement security, logging or some other sort of logic we would just add code to the avatars method in UserController. For example, avatars could return a 401 (unauthorized) error if the user wasn’t logged in, or didn’t have access to view Mickey’s image for some reason.

That’s it for now; next time I’ll modify this sample app once more to demonstrate how we can store the image files in a database table, instead of in the “non-public” folder or anywhere on the file system.

Tags:·

17 responses so far ↓

  • 1 Ceres // Jul 08, 2009 at 06:17 PM

    Awesome tutorial... Thanks for this.
  • 2 heckubiss // Aug 27, 2009 at 10:39 PM

    what if I still want to have an index pages show all users that start with the letter A. Now that I am doing it through a Controller it seems impossible to show the avatars of the other users even if I am a logged in user
  • 3 pat // Aug 28, 2009 at 08:13 AM

    In your controller, you could use ActiveRecord to query the users you want from the database; in your example you could use code like this:

    @users = User.find(:all, :conditions => 'name LIKE "A%"')

    Then in your view you could use the @users array to display the users’ names, avatars or whatever you want. How were you doing this before downloading files using Apache? Were you displaying an Apache generated index page? Possibly I’m misunderstanding your question…

  • 4 heckubiss // Aug 29, 2009 at 12:57 AM

    It doesnt like that statment. I get this error Mysql::Error: Unknown column 'name' in 'where clause': SELECT * FROM `users` WHERE (name LIKE "A%")
  • 5 heckubiss // Aug 29, 2009 at 01:00 AM

    Oops.. sorry i had the wrong field.. when i use 'login' instead of name it works fine
  • 6 Mo // Dec 22, 2009 at 08:10 PM

    Great tutorial. Do you know how one could upload an image/file via paperclip using a REST based API?
  • 7 Mo // Dec 22, 2009 at 08:13 PM

    One more thing. I tried following your tutorial, but when the file is saved, it is saved as images1.jpg?1261529656 instead of image1.jpg So I image does not get displayed in the web page. I just see the Images1 name instead of the picture. Anyone know why?
  • 8 pat // Dec 22, 2009 at 09:14 PM

    Hi Mo,

    With regard to REST for Paperclip: I’m not sure how you would actually upload a file using REST. To send a file to the web server, you need to POST using a multipart/form-data encoded form. But once the file is on the server, you could access its Paperclip file and attributes very easily using the code produced by the standard Rails scaffolding (respond_to… format.xml). For example, you could try it in my sample app (or in your own app if you created it using scaffolding) by just adding “.xml” onto the end of the URL. That URL would be for example: http://localhost:3000/users/2.xml. It would be easy to also add the file’s URL as another XML tag.

    So… build a REST app the way you normally would with Rails and just include the Paperclip files and their attributes (name, size, etc.) as additional XML info. But for the actual file upload, you’ll need a file upload form as usual.

    About the name of the image appearing and not the image: check your log file… is there an error there? Maybe your “:path” setting in the has_attached_file line is wrong, or for some other reason the actual location of your files is out of sync with where Paperclip is looking for them. I got a similar error when the file Paperclip was looking for didn’t exist (I had long ago deleted it). The number you see is just a timestamp Paperclip adds to the end of the URL, but you should not see that in the file’s actual name on disk. Let me know if this helps...

  • 9 Daniel // Feb 03, 2010 at 11:19 PM

    Thanks man! Cool tutorial!
  • 10 Steve // Feb 06, 2010 at 08:19 AM

    Thank you for the very thorough walkthrough - you've helped me tremendously! Keep up the great work.
  • 11 Chris Whamond // May 12, 2010 at 12:16 AM

    Pat: Thanks for your awesome explanation, but I’m stuck with what is probably a simple error.

    I’m getting an Unknown Action (No action responded to 30) - which means my controller is messed up (I think the routes are okay). It’s seeing the id as the action.

    I have namespaced routes: members/downloads

    I can upload and the attachments go into the correct /non-public/system… folders. But when I click my link to download (or open) the attachment, I get that error.

    I’ve posted details at http://www.railscheatsheet.com/snippets/217

    Thanks for your help! Great tutorial.

  • 12 pat // May 12, 2010 at 11:36 AM

    Thanks Chris; glad to hear you found the tutorial helpful!

    It looks like the problem is mismatch between the singular/plural action name, and has nothing to do with your namespaced routes. Try using the plural action name in your routes file:

    map.namespace :members do |members|
        members.resources :downloads, :member => { :ttrs => :get }
    end
    


    That’s “:ttrs” and not “:ttr”. The way Paperclip works is that when you use the :attachment interpolation inside the :url option for has_attached_file, it generates a url using the plural version of the attachment name. So you need :ttrs in your route, and not :ttr. You correctly have the plural action name in your controller code.

  • 13 Chris Whamond // May 13, 2010 at 04:16 PM

    Thanks again - Pat. That did the trick! Working flawlessly now.

  • 14 Tom // May 25, 2010 at 06:02 AM

    Hi Thanks for this. I am getting a problem. I can’t display images in index and show For example in index I get like Avatars?style=thumb&1274781314

    But when I access localhost:3000/users/1/avatars?thumb it works perfectly. My rails version is 2.3.5

    My model is hasattachedfile :avatar, :styles => { :thumb => “75x75>”, :small => “150x150>” }, :path => ‘:rails_root/non-public/system/:class/:attachment/:id/:style/:basename.:extension’, :url => ‘:class/:id/:attachment?style=:style’

  • 15 pat // May 25, 2010 at 07:03 AM

    Hi Tom – can you post your show or index view code here, on pastie.org or in an email? Possibly your image tag is referring to the image file name and not the image URL? If the images are downloading properly then the model and controller are probably fine. Try to send me enough code to reproduce the problem…

  • 16 Tom // May 25, 2010 at 07:15 AM

    Hi
    Thanks for your sudden reply. I am pasting details here http://pastie.org/976017

  • 17 pat // May 25, 2010 at 04:20 PM

    Lol… I’m not usually so responsive; you just caught me at a convenient time this morning.

    So I took a look at your code: try making the “:url” value you pass to has_attached_file absolute; like this:

    has_attached_file :avatar,
      :styles => {
        :thumb => "75x75>",
        :small => "150x150>"
       },
       :url => '/:class/:id/:attachment?style=:style',
       :path => etc...
    end
    


    Note the leading slash before “:class” …otherwise if you give a relative URL value to image_tag it will prepend “/images/…” and you’ll have the wrong URL in your page, meaning the request never gets to your controller action. With the leading slash, image_tag just passes the absolute URL into the HTML unchanged.

Leave a Comment