Since Thoughtbot released it a couple of years ago, Paperclip has practically become synonymous with file upload for many Rails developers. Out of the box, Paperclip saves uploaded files in your application’s public
directory, which makes good sense for files that should be accessible to everyone—by storing them there, you can serve them without the additional overhead of Rails. But what if you don’t want the world at large to access uploaded files? In that case, that additional overhead becomes an asset. With a couple of additional parameters sent to Paperclip, and some modified controller code, you’ll be able to keep uploaded files safe from prying eyes.
For this tutorial, I’m going to assume you’re familiar with Paperclip, as there are plenty of tutorials already out there for using it. Refer to the Railscasts episode on Paperclip to get up to speed. It would also be a good idea if you’re familiar with Rails authentication using Devise, Authlogic, Restful Authentication, etc.—this tutorial is authentication-agnostic, but authenticating users before allowing downloads is kind of the point here.
Finally, I’m using Haml for markup and starting from scaffolds generated by the Nifty Generators gem—see my tutorial on using nifty_scaffold if interested, but this really only applies to the sample views.
To demonstrate this, let’s imagine a simple application. We want to allow people to share documents with one another, but some of those documents may be sensitve—we thus need to make sure users are logged in before they can download them. Since we can’t protect files saved in the application’s public
directory to this level, we’ll need to go beyond Paperclip’s default options. To do this, we’ll take a generated scaffold, then modify its model and controller to allow us verify a user is logged in before downloading a file.
You could also sprinkle in some authorization here—for example, I can only post an updated version of a file if I’m the one who posted it in the first place—but that’s beyond the scope of this tutorial. If you needed to do this you’d use an authorization system like Declarative Authorization, CanCan, or RESTful_ACL.
Let’s begin with the Document model. I’m adding one option to Paperclip’s standard has_attached_file
here: I’m overriding the default save path—instead of my public
directory, I’m saving to a directory called uploads
, which gets created at my application’s root level.
Jumping over to the controller, there are two changes of note:
before_filter
to verify that someone is logged in. This example uses Restful Authentication’s :login_required
; replace as needed with your preferred login system’s filter.show
generated by my scaffold, so that instead of returning HTML code to my browser it returns the file. I do this by using basic Ruby File
methods—first I open the file at the specified document’s path, then I use send_data
to push the binary data back to the web browser.Update: As pointed out by James Bebbington, there’s a better way to return files to the browser. I’ve commented out my original method and replaced it with a more appropriate one—sorry, I let some code carry over from an older project that retrieved binary data from a database. For more information on this correct method, including using the X-SendFile server module to offload the file transfer from your app to your web server, see File Downloads Done Right>
For completion’s sake, here is what my index
view looks like for the Documents controller. Since I’m using a basic RESTful scaffold, this is pretty straightforward. I’ve added a couple of extra columns to display the content type and file size of each document.
At this point you could delete the show.html.haml
view file; it’s not needed given how we’ve modified the controller. Or you could hold off on deleting it after you read the section “A slightly fancier example” below.
Finally, you’ll want to tell your version control system to ignore files that get added to your application’s uploads
directory. Here’s how to add it to a .gitignore
file:
So far, we’ve used standard Rails RESTful scaffolding techniques to serve up files. This works because we don’t otherwise need the show
method to return anything to the browser. If we did need to use it—say, to display more information about the document, or add an interface for our users to add comments, we’d need to change a couple of things.
First, in the model, we need to change the URL used to download the file. I’m going to point it to a download
method that will be in documents_controller.rb
:
Then we need to add that new download
method to the controller. Note this is the same code we were using earlier in show
; now, since we need show
to do typical show
-type stuff, we just need to move this code elsewhere.
As noted above, my original controller code worked but was not optimal. The old code is commented out here with the correct send_file
method now being used.
Don’t forget to add download
as a member route to documents
in your routes.rb
file.
Finally, here’s what the show
view might look like—let’s just add a simple Download link:
That’s all there is to it—now, instead of exposing uploaded files to the world by sticking them in your public
directory, you’ve got some extra Rails logic to make sure only the right people can access them.
Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.