Ruby
Article

Uploading Files with Paperclip

By Ilya Bodrov-Krukowski

Screenshot 2015-07-01 08.53.31

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 _content_type 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!

  • http://duykhoa.tenluaweb.com kevin.tran

    I created a storage options for paperclip. So instead of using the same storage for development env and production, staging env, you can separate them to different kind of storage. That makes sense, cause you don’t need to upload to S3 when you are playing with development version only.

    https://gist.github.com/duykhoa/0f433b88fd57871b28fb

    • Ilya Bodrov

      Yeah, that seems like a nice idea!

  • Greg Blass

    The problem with paperclip is that there is no support for multiple file uploads at once. This quickly became a feature that our clients requested at my previous company. In subsequent projects I used carrierwave and jquery-file-upload to give a gmail like file uploading experience and it has worked well.

  • absessive

    I’m trying to do this but this is what I end up getting
    https://gist.github.com/absessive/86c801c4994107197c93

    A Tempfile is being created and there Photo object doesn’t have the right title. The title is

    #

  • Zora Shariff

    Paperclip doesn’t work with more than one files. This makes the feature a problem while there are other tools with which one can upload multiple files here. So, the authors should give attention towards finding solution to the problem.

  • https://skyfeedback.com Boris Tkachev

    What about if controller already has destroy method, to destroy posts. Can there be 2 destroy methods? one for posts and another for images?

    • Ilya Bodrov

      Yes, of course! However, I’d stay away from this solution and instead employed two separate controllers – one to manage posts and another one to manage images. Having lots of non-standart action inside a controller is not generally recommended.

  • Rahul Kumar

    Does any one knows how to make “choose file” button here fancy when using form_for (file_fields.)

  • Rahul Kumar

    Does any one knows how to make “choose file” button here fancy when using form_for (file_fields.)

    • Ilya Bodrov

      There are handful of techniques. The general idea is hiding the original button and showing the fake, styled button instead. Just google it, I believe StackOverflow have some answers.

  • Idriss SACKO

    Hi Ilay, I have this when gerenerte pdf:
    ArgumentError (/options/avatars/1/original/bara.jpg not found):

    2015-12-28T12:08:03.290073+00:00 app[web.1]: app/pdfs/FormatPages.rb:81:in `header’

    2015-12-28T12:08:03.290074+00:00 app[web.1]: app/pdfs/produits_pdf.rb:10:in `initialize’

    2015-12-28T12:08:03.290074+00:00 app[web.1]: app/controllers/produits_controller.rb:18:in `new’

    2015-12-28T12:08:03.290075+00:00 app[web.1]: app/controllers/produits_controller.rb:18:in `block (2 levels) in index’

    • Ilya Bodrov

      Hi. Sorry, but I don’t really understand what is the role of a PDF file here. Are you trying to generate pdf from an image? WIthout seeing the actual code I won’t be able to help.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.