Uploading Files with Paperclip
Some time ago I wrote Better File Uploads with Dragonfly and Asynchronous File Uploads in Rails, two articles that covered Dragonfly, a nice gem to handle various attachments. In this article I am going to introduce Paperclip by Thoughtbot – probably, the most popular and feature-rich solution for integrating file uploading and management into an application.
One of the great things about Paperclip is its large and active community – this gem is constantly being updated and that’s really important. You don’t want to use a solution that will be abandoned in a year, do you?
We are going to observe all the main features of Paperclip and create a pretty simple but useful app featuring:
- Basic file uploading
- Validations and callbacks
- Post-processing (with thumbnail generation)
- Amazon S3
I will give you all necessary info to start off really quick and also present links for further reading.
The source code for the demo app can be found on GitHub.
The working example of the demo app can be accessed at sitepoint-paperclip-uploader.herokuapp.com.
Some Preparations
We are going to craft an app that allows users to upload their own photos, as well as browse photos added by others.
Create a new Rails app called SimpleUploader
:
$ rails new SimpleUploader -T
I am using Rails 4 here, but Paperclip is compatible with Rails 3.2 and even Rails 2 (just use Papeclip 2.7, in that case).
I am going to use Bootstrap for basic styling, but you may skip this step:
Gemfile
[...]
gem 'bootstrap-sass'
[...]
Run
$ bundle install
And update the necessary style files:
application.scss
@import 'bootstrap-sprockets';
@import 'bootstrap';
Now, tweak the layout to include a top menu and style the main content:
layouts/application.html.erb
[...]
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Simple Uploader', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><%= link_to 'Photos', root_path %></li>
<li><%= link_to 'Upload Your Photo', new_photo_path %></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>">
<%= value %>
</div>
<% end %>
<%= yield %>
</div>
[...]
We are only going to have a single controller:
photos_controller.rb
class PhotosController < ApplicationController
end
Set up the corresponding routes:
config/routes.rb
[...]
resources :photos, only: [:new, :create, :index, :destroy]
root to: 'photos#index'
[...]
Let’s create our Photo
model. For now, add just a title
attribute… we’ll take care of the other ones later:
$ rails g model Photo title:string
$ rake db:migrate
Basically, the preparations are done. Now we can integrate Paperclip and flesh out the controller. On to the next section!
Implement File Upload in 60 seconds
System Requirements
Before proceeding, spend a couple of minutes to check if your system is ready for Paperclip. A few things have to be done:
- Ruby 2.0 or higher has to installed and Rails 3.2 or 4 has to be used. You may still use Ruby 1.8.7 or Rails 2, but for that, stick to Paperclip 2.7. This version has a separate readme and I am not going to cover it here.
- Windows users have to install DevKit.
- ImageMagick, a great utility to work with images, has to be installed. Binary releases for all major platforms are available.
- The
file
command should be available. Windows does not have it, but DevKit provides it. Under some circumstances, however, you will have to add it manually – read more here.
When all items in this small checklist are done, proceed to the next step!
Integrating Paperclip
You do not have to be a superhero to integrate file uploading functionality into your app in one minute – just use Rails and Paperclip. Seriously, it is really easy to get started. First of all, include the gem:
Gemfile
[...]
gem "paperclip", "~> 4.2"
[...]
and install it:
$ bundle install
Now, we need to add some attributes to the photos
table. You may generate a simple migration for this, but Paperclip has a nice generator:
$ rails generate paperclip photo image
Here is what the resulting migration looks:
xxx_add_attachment_image_to_photos.rb
class AddAttachmentImageToPhotos < ActiveRecord::Migration
def self.up
change_table :photos do |t|
t.attachment :image
end
end
def self.down
remove_attachment :photos, :image
end
end
As you can see, Paperclip even presents its own attachment
method and that’s really convenient. What does this method actually do? It adds the following columns into your table:
{attachment}_file_name
{attachment}_file_size
{attachment}_content_type
{attachment}_updated_at
The name of the attachment field is the argument provided to the attachment
method (image
in our case). Actually, only the {attachment}_file_name
field is required, so if you don’t want to store other information, go ahead and create a basic migration instead.
Now apply the migration:
$ rake db:migrate
and equip your model with Paperclip functionality:
models/photo.rb
[...]
has_attached_file :image
[...]
Set up the controller:
photos_controller.rb
class PhotosController < ApplicationController
def index
@photos = Photo.order('created_at')
end
def new
@photo = Photo.new
end
def create
@photo = Photo.new(photo_params)
if @photo.save
flash[:success] = "The photo was added!"
redirect_to root_path
else
render 'new'
end
end
private
def photo_params
params.require(:photo).permit(:image, :title)
end
end
Note that you only have to permit image
, not image_file_name
or other fields. Those are used internally by Paperclip.
The view for the new
action:
views/photos/new.html.erb
<div class="page-header"><h1>Upload Photo</h1></div>
<%= form_for @photo do |f| %>
<%= render 'shared/errors', object: @photo %>
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :image %>
<%= f.file_field :image, class: 'form-control'%>
</div>
<%= f.submit 'Upload', class: 'btn btn-primary' %>
<% end %>
I prefer to extract error messages to a separate partial:
views/shared/_errors.html.erb
<% if object.errors.any? %>
<div class="panel panel-warning errors">
<div class="panel-heading">
<h5><i class="glyphicon glyphicon-exclamation-sign"></i> Found errors while saving</h5>
</div>
<ul class="panel-body">
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
Okay, so as you can see, the form is really simple. Just use the f.file_field :image
to render the appropriate field. For Rails 3, you’ll also have to provide :html => { :multipart => true }
to the form_for
method.
However, if you try to upload a file now, an exception will be raised. Why? Because Paperclip cares about the security of your app like no one else does! Starting from version 4, it is required to have at least a content type validation in your model, so let’s add it:
models/photo.rb
[...]
validates_attachment :image,
content_type: { content_type: ["image/jpeg", "image/gif", "image/png"] }
[...]
Please note that, for this validation to work, your table must have an
column.
But, what if we’re seasoned developers and can take care of ourselves? Can we tell Paperclip that our attachments should not be validated? But, of course:
do_not_validate_attachment_file_type :avatar
However, add this line only if you really understand what you are doing.
With validations in place (or after disabling it explicitly) you can upload your first file. Before doing that, however, let’s create the index
view:
views/photos/index.html.erb
<div class="page-header"><h1>Photos</h1></div>
<%= render @photos %>
We are simply iterating over the @photos
array. The _photo.html.erb partial will be used by default, so let’s create it:
views/photos/_photo.html.erb
<div class="media">
<div class="media-left">
<%= link_to image_tag(photo.image.url, class: 'media-object'), photo.image.url, target: '_blank' %>
</div>
<div class="media-body">
<h4 class="media-heading"><%= photo.title %></h4>
</div>
</div>
To display an image we are using the good old image_tag
method. image.url
, as you’ve guessed, returns the URL to the original image.
Go ahead and upload something. Everything should be working fine, but there is more to be done, so let’s proceed to the next step.
Generating Thumbnails and Post-Processing
If you upload a large image, it will totally break your page layout. We could apply some simple styles to shrink large images inside .media
blocks, but that’s not quite optimal. Users will still have to download the full version of the image that will then be resized. Why waste bandwidth like this? Let’s generate thumbnails instead with the help of ImageMagick!
Each attachment can have a set of “styles” that define various versions of an original image. Let’s add one:
models/photo.rb
[...]
has_attached_file :image,
styles: { thumb: ["64x64#", :jpg] }
[...]
What does this all mean? thumb
is the name of the “style”. 64x64
, as you’ve guessed, means that this version of the image be sized to 64×64 pixels. #
means that ImageMagick should crop the image if it is larger than the provided dimensions (with center gravity).
If you browse this page you won’t find this option, but Paperclip adds it on its own. By the way, Dragonfly does the same thing.
:jpg
means that the original file should be converted to a JPEG. Really neat and clean.
You can re-generate thumbnails according to the new styles with the following command:
$ rake paperclip:refresh:thumbnails CLASS=User
To re-generate all images (including original ones) use:
$ rake paperclip:refresh CLASS=Photo
To generate only the missing images:
$ rake paperclip:refresh:missing_styles
Now, update the partial:
views/photos/_photo.html.erb
[...]
<%= link_to image_tag(photo.image.url(:thumb), class: 'media-object'), photo.image.url, target: '_blank' %>
[...]
We just provide the style’s name to the url
method. Check out the result!
Let’s add some more post-processing options:
models/photo.rb
[...]
has_attached_file :image,
styles: { thumb: ["64x64#", :jpg],
original: ['500x500>', :jpg] },
convert_options: { thumb: "-quality 75 -strip",
original: "-quality 85 -strip" }
[...]
original
means that the following styles need to be applied to the original image. 500x500>
means that the image should be resized to 500×500 and shrunk, if required. If the image’s dimensions are smaller, it will be left intact.
With the help of convert_options
you can pass additional parameters. In this example, we say that the image quality should be rescaled to 75% (thumb) and 85% (original) accordingly to reduce their size. The -strip
option tells ImageMagick to strip out all meta information.
Regenerate all images:
$ rake paperclip:refresh CLASS=Photo
and observe the results.
You can easily define styles dynamically by passing lambda
to styles
:
has_attached_file :image, :styles => lambda { |attachment| { :thumb => (attachment.instance.title == 'Special' ? "100x100#" : "64x64#") } }
In this pretty naive example, we simply check the photo’s title and, if it is “Special”, use other dimensions for styling.
Paperclip presents you with convenient callbacks before_post_process
and after_post_process
to do work before and after post-processing. You may even define attachment-specific callbacks:
before_{attachment}_post_process
after_{attachment}_post_process
The classic example for callbacks usage is checking whether the file is an image or not. Paperclip’s documentation presents the following snippet:
before_post_process :skip_for_audio
def skip_for_audio
! %w(audio/ogg application/ogg).include?(asset_content_type)
end
If the before
callback returns false
, post-processing won’t occur. Read more here.
It’s worth mentioning that you can write your own processor and store it inside lib/paperclip_processors – Paperclip automatically loads all files placed here. Read more here.
Validations
Let’s return to our validations and add a couple more. For example, I want all photos to have their images set and their sizes to be no more than 500 KB. That’s easy:
models/photo.rb
validates_attachment :image, presence: true,
content_type: { content_type: ["image/jpeg", "image/gif", "image/png"] },
size: { in: 0..500.kilobytes }
By the way, Paperclip also automatically prevents content type spoofing – that is, you can’t upload HTML file as JPEG for example. More can be found here.
Another thing to remember is that post-processing won’t start if the model is not valid, that is if validation failed.
Storing the Images
By default all images are placed inside the public/system directory of your Rails project. You probably don’t want users to see that path and obfuscate it somehow. Paperclip can take care of it as well!
models/photo.rb
[...]
has_attached_file :image,
url: "/system/:hash.:extension",
hash_secret: "abc123"
[...]
hash_secret
has to set for this to work.
Another question you might ask is, what happens to the images when the corresponding record is destroyed? Find that out by adding a destroy
method to the controller
photos_controller.rb
[...]
def destroy
@photo = Photo.find(params[:id])
@photo.destroy
flash[:success] = "The photo was destroyed."
redirect_to root_path
end
[...]
and modifying the partial:
views/photos/_photo.html.erb
<div class="media">
<div class="media-left">
<%= link_to image_tag(photo.image.url(:thumb), class: 'media-object'), photo.image.url, target: '_blank' %>
</div>
<div class="media-body">
<h4 class="media-heading"><%= photo.title %></h4>
<%= link_to 'Remove', photo_path(photo), class: 'btn btn-danger', method: :delete, data: {confirm: 'Are you sure?'} %>
</div>
</div>
Remove some photos – the corresponding images should be destroyed as well. This behavior, however, can be changed by setting preserve_files
to true
:
models/photo.rb
[...]
has_attached_file :image,
preserve_files: "true"
[...]
This is convenient when using something like actsasparanoid for soft deletion of records.
Sitting on the Cloud
Can we store images in a CDN, you ask? Yes, yes we can. In this demo, I am going to show you how to use Amazon S3 as file storage. Paperclip does support other storage options, like Dropbox.
Drop in the new gem:
Gemfile
[...]
gem 'aws-sdk', '~> 1.6'
[...]
and run
$ bundle install
At the time of this writing, Paperclip did not support AWS SDK version 2, so don’t use it yet! Support for it should soon be added though.
Now modify your model (remove url
and hash_secret
options as well):
models/photo.rb
[...]
has_attached_file :image,
styles: { thumb: ["64x64#", :jpg],
original: ['500x500>', :jpg] },
convert_options: { thumb: "-quality 75 -strip",
original: "-quality 85 -strip" },
storage: :s3,
s3_credentials: {access_key_id: ENV["AWS_KEY"], secret_access_key: ENV["AWS_SECRET"]},
bucket: "YOUR_BUCKET"
[...]
Actually, s3_credentials
may accept a path to a YAML file with access_key_id
and secret_access_key
. What’s more, you can provide different values for various environments like this:
development:
access_key_id: 123...
secret_access_key: 123...
test:
access_key_id: abc...
secret_access_key: abc...
production:
access_key_id: 456...
secret_access_key: 456...
That would an overkill in our case, though. Read more here.
Now, where to get those keys? Register at AWS and navigate to Security Credentials (dropdown menu at the top right corner). Next open the Users tab and click the Create New Users button. We are going to create a special service user account that will only have permissions to work with S3. Prior to introducing this system, we had to use the root key, which sn’t very secure. Anyone who gains access to this key had full access to all your AWS services. Root keys are still supported by Amazon, but I really don’t recommend using them.
After clicking on the Create New Users enter the user’s name and check the “Generate an access key for each user”. Click Create.
Now click “Show User Security Credentials”, copy and paste the Access Key ID and Secret Access Key into your model. This will be the last time the user’s credentials will be available for download, so you may also want to download and store them in a secure place. Be very careful with this key pair and never expose it (specifically, it should not be visible in GitHub). When deploying on Heroku, I am using environment variables to store this key pair.
When you are done, go to the Groups page and click Create New Group. Enter the group’s name (something like “accesstos3″).
Click Next and enter “S3” in the search form on the next page. This is the page to select policies for the group. Choose “AmazonS3FullAccess” and click Next and Create. By the way, you can create your own policies on the corresponding page to define custom permissions (for example, if you want to grant someone access to billing information – there is no such policy by default).
Click on the newly create Group and click Add Users to Group. Next, just select your new user from the list and click Add Users. Now your user has all the necessary permissions to work with S3. If you’ve worked with Active Directory before, the process should be very familiar.
Also do not forget to open the S3 Management Console and create a new bucket to store your images (you may think of buckets as simple folders). Provide the bucket’s name in the bucket
option in your model file.
Basically, you are good to go. Try uploading some files using your new storage!
Please note that, by default, those files are being loaded with public_read
permissions, which means that anyone can view them simply by entering the correct URL. Using the s3_permissions
option you can change that (Here is the information about S3 permissions).
Paperclip’s wiki also presents a nice guide on restricting access to S3 images. Basically it suggests setting s3_permissions
to private
and using a special download
action in the controller with expiring URL.
The last thing to note is that, in general, S3 is a paid service. For the first year, however, you get 5GB of storage space, 20000 GET, and 2000 PUT requests per month for free. Full pricing information can be found here.
Conclusion
In this article we’ve discussed Paperclip and its various features, like post-processing, callbacks, URL obfuscation, and support for various storage options. If you’ve never used Paperclip in your projects, I really recommend giving it a try when a need for file uploading support arises.
As always, feedback is very much welcome and don’t hesitate to ask your questions. Thanks for reading and see you soon!