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_filterto verify that someone is logged in. This example uses Restful Authentication’s
:login_required; replace as needed with your preferred login system’s filter.
showgenerated by my scaffold, so that instead of returning HTML code to my browser it returns the file. I do this by using basic Ruby
Filemethods—first I open the file at the specified document’s path, then I use
send_datato 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
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
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
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.
I stand with the Black community against systemic racism, police violence and brutality, intolerance, and hate in the United States and worldwide. We must all demand better from our leaders, and ourselves. Stop tolerating intolerance.
While you're here, please consider making a donation to Black Girls CODE, who do great, important work to provide opportunity to underprivileged girls interested in tech, or any organization working toward equity and safety for all, not just the privileged. Thank you.
If you liked my series on practical advice for adding reliable tests to your Rails apps, check out the expanded ebook version. Lots of additional, exclusive content and a complete sample Rails application.
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.