Uploading With Rails and Paperclip

This is the last article in the “Uploading with Rails” series. In the past couple of months we have already discussed the Shrine, Dragonfly, and Carrierwave gems. Today’s guest is Paperclip by Thoughtbot, a company which manages gems such as FactoryGirl and Bourbon.

Uploading With Rails and Paperclip

Paperclip is probably the most popular attachment management solution for Rails (more than 13 million downloads), and for a good reason: it has lots of features, a great community, and thorough documentation. So hopefully you are eager to learn more about this gem!

In this article you will learn how to:

  • Prepare for Paperclip installation
  • Integrate Paperclip into a Rails application
  • Add attachment validations
  • Generate thumbnails and process images
  • Obfuscate URLs
  • Store attachments on Amazon S3
  • Secure files in the cloud by introducing authorization logic

The source code for this article is available on GitHub.

Preparations

Before we dive into the code, let’s firstly discuss some caveats that you need to know about in order to successfully work with Paperclip:

  • The latest version of Paperclip supports Rails 4.2+ and Ruby 2.1+. This gem can also be used without Rails.
  • ImageMagick must be installed on your PC (it is available for all major platforms), and Paperclip should be able to access it.
  • The file command should be available from the command line. For Windows it is available via Development Kit, so follow these instructions if you don’t have DevKit installed yet.

When you are ready, go ahead and create a new Rails application (I will be using Rails 5.0.2) without the default testing suite:

rails new UploadingWithPaperclip -T

Integrating Paperclip

Drop in the Paperclip gem:

Gemfile

gem "paperclip", "~> 5.1"

Install it:

bundle install

Suppose we are creating a bookshelf application that presents a list of books. Each book will have a title, a description, an author’s name, and a cover image. To start off, generate and apply the following migration:

rails g model Book title:string description:text image:attachment author:string
rails db:migrate

Note the attachment type that is presented for us by Paperclip. Under the hood, it is going to create four fields for us:

  • image_file_name
  • image_file_size
  • image_content_type
  • image_updated_at

In contrast to the Shrine and Carrierwave gems, Paperclip does not have a separate file with configurations. All settings are defined inside the model itself using the has_attached_file method, so add it now:

models/book.rb

has_attached_file :image

Before proceeding to the main part, let’s also create a controller along with some views and routes.

Creating the Controller, Views, and Routes

Our controller will be very basic:

books_controller.rb

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :download]

  def index
    @books = Book.order('created_at DESC')
  end

  def new
    @book = Book.new
  end

  def show
  end

  def create
    @book = Book.new(book_params)
    if @book.save
      redirect_to books_path
    else
      render :new
    end
  end

  private

  def book_params
    params.require(:book).permit(:title, :description, :image, :author)
  end

  def set_book
    @book = Book.find(params[:id])
  end
end

Here is an index view and a partial:

views/books/index.html.erb

<h1>Bookshelf</h1>

<%= link_to 'Add book', new_book_path %>
<ul>
  <%= render @books %>
</ul>

views/books/_book.html.erb

<li>
  <strong><%= link_to book.title, book_path(book) %></strong> by <%= book.author %>
</li>

Now the routes:

config/routes.rb

Rails.application.routes.draw do
  resources :books
  root to: 'books#index'
end

Nice! Now let’s proceed to the main section and code the new action and a form.

Uploading Files

All in all, doing uploads with Paperclip is easy. You only need to permit the corresponding attribute (in our case that’s the image attribute, and we’ve already permitted it) and present a file field in your form. Let’s do it now:

views/books/new.html.erb

<h1>Add book</h1>

<%= render 'form', book: @book %>

views/books/_form.html.erb

<%= form_for book do |f| %>
  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>

  <div>
    <%= f.label :author %>
    <%= f.text_field :author %>
  </div>

  <div>
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>

  <div>
    <%= f.label :image %>
    <%= f.file_field :image %>
  </div>

  <%= f.submit %>
<% end %>

With this setup, you can already start performing uploads, but it’s a good idea to introduce some validations as well.

Adding Validations

Validations in Paperclip can be written using old helpers like validates_attachment_presence and validates_attachment_content_type or by employing the validates_attachment method to define multiple rules at once. Let’s stick with the latter option:

models/book.rb

  validates_attachment :image,
                       content_type: { content_type: /Aimage/.*z/ },
                       size: { less_than: 1.megabyte }

The code is really simple, as you can see. We require the file to be an image less than 1 megabyte in size. Note that if the validation fails, no post-processing will be performed. Paperclip already has some errors messages set for the English language, but if you want to support other languages, include the paperclip-i18n gem into your Gemfile.

Another important thing to mention is that Paperclip requires you to validate content type or filename of all attachments, otherwise it will raise an error. If you are 100% sure you don’t need such validations (which is a rare case), use do_not_validate_attachment_file_type to explicitly say which fields shouldn’t be checked.

Having added validations, let’s also display error messages in our form:

views/shared/_errors.html.erb

<% if object.errors.any? %>
  <h3>Some errors were found:</h3>
  <ul>
    <% object.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

views/books/_form.html.erb

<%= render 'shared/errors', object: book %>

Displaying Images

Okay, so now the uploaded images should be displayed somehow. This is done by using the image_tag helper and a url method. Create a show view:

views/books/show.html.erb

<h1><%= @book.title %> by <%= @book.author %></h1>

<%= image_tag(@book.image.url) if @book.image.exists? %>

<p><%= @book.description %></p>

We are displaying an image only if it really exists on the drive. Moreover, if you are using cloud storage, then Paperclip will perform a network request and check the file’s existence. Of course, this operation may take some time, so you might use the present? or file? methods instead: they will simply make sure that the image_file_name field is populated with some content.

URI Obfuscation

By default, all attachments are stored inside the public/system folder, so you will probably want to exclude it from the version control system:

.gitignore

public/system

However, displaying a full URI to the file may not always be a good idea, and you might need to obfuscate it somehow. The easiest way to enable obfuscation is by providing two parameters to the has_attached_file method:

models/book.rb

url: "/system/:hash.:extension",
hash_secret: "longSecretString"

The proper values will be interpolated into the url automatically. hash_secret is a required field, and the easiest way to generate it is by using:

rails secret

Working With Styles

In many cases, it is preferred to display an image’s thumbnail with some predefined width and height to save bandwidth. Paperclip solves this by using styles: each style has a name and a set of rules, like dimensions, format, quality, etc.

Suppose that we want the original image and its thumbnail to be converted to JPEG format. The thumbnail should be cropped to 300x300px:

models/book.rb

  has_attached_file :image,
                    styles: {
                        thumb: ["300x300#", :jpeg],
                        original: [:jpeg]
                    }

# is a geometry setting meaning: “Crop if necessary while maintaining aspect ratio.”

We can also provide additional conversion options for each style. For example, let’s provide 70% quality for thumbs while removing all metadata and 90% quality for the original image to make it a bit smaller:

models/book.rb

  has_attached_file :image,
                    styles: {
                        thumb: ["300x300#", :jpeg],
                        original: [:jpeg]
                    },
                    convert_options: {
                        thumb: "-quality 70 -strip",
                        original: "-quality 90"
                    }

Nice! Display the thumbnail and provide the link to the original image:

views/books/show.html.erb

<%= link_to(image_tag(@book.image.url(:thumb)), @book.image.url, target: '_blank') if @book.image.exists? %>

Note that unlike Carrierwave, for example, Paperclip does not allow you to write @book.image.thumb.url.

If, for some reason, you wish to manually update uploaded images, then you may use the following commands to refresh only thumbnails, add missing styles, or refresh all images:

  • rake paperclip:refresh:thumbnails CLASS=Book
  • rake paperclip:refresh:missing_styles CLASS=Book
  • rake paperclip:refresh CLASS=Book

Storing Files in the Cloud

Like all similar solutions, Paperclip allows you to upload files to the cloud. Out of the box, it has support for the S3 and Fog adapters, but there are third-party gems for Azure and Dropbox as well. In this section, I will show you how to integrate Paperclip with Amazon S3. First, drop in the aws-sdk gem:

gem 'aws-sdk'

Install it:

bundle install

Next, provide a new set of options to the has_attached_file method:

models/book.rb

  has_attached_file :image,
                    styles: {
                        thumb: ["300x300#", :jpeg],
                        original: [:jpeg]
                    },
                    convert_options: {
                        thumb: "-quality 70 -strip",
                        original: "-quality 90"
                    },
                    storage: :s3,
                    s3_credentials: {
                        access_key_id: ENV["S3_KEY"],
                        secret_access_key: ENV["S3_SECRET"],
                        bucket: ENV["S3_BUCKET"]
                    },
                    s3_region: ENV["S3_REGION"]

Here I am sticking to the dotenv-rails gem to set environment variables. You may provide all values directly inside the model, but do not make it publicly available.

What’s interesting is that s3_credentials also accepts a path to a YAML file containing your keys and a bucket name. Moreover, you can set different values for different environments like this:

development:
  access_key_id: key1
  secret_access_key: secret1
production:
  access_key_id: key2
  secret_access_key: secret2

That’s it! All the files you upload will now be located in your S3 bucket.

Securing Files in the Cloud

Suppose you don’t want your uploaded files to be available to everyone. By default, all uploads into the cloud are marked as public, meaning that anyone can open the file via the direct link. If you wish to introduce some authorization logic and check who is able to view the file, set the s3_permissions option to :private like this:

  has_attached_file :image,
                    styles: {
                        thumb: ["300x300#", :jpeg],
                        original: [:jpeg]
                    },
                    convert_options: {
                        thumb: "-quality 70 -strip",
                        original: "-quality 90"
                    },
                    storage: :s3,
                    s3_credentials: {
                        access_key_id: ENV["S3_KEY"],
                        secret_access_key: ENV["S3_SECRET"],
                        bucket: ENV["S3_BUCKET"]
                    },
                    s3_region: ENV["S3_REGION"],
                    s3_permissions: :private

Now, however, no one except for you will be able to see the files. Therefore, let’s create a new download action for the BooksController:

books_controller.rb

  def download
    redirect_to @book.image.expiring_url
  end

This action will simply redirect users to the image via an expiring link. Using this approach, you can now introduce any authorization logic using gems like CanCanCan or Pundit.

Don’t forget to set the member route:

config/routes.rb

  resources :books do
    member do
      get 'download'
    end
  end

The helper should be used like this:

link_to('View image', download_book_path(@book), target: '_blank')

Conclusion

We’ve come to the end of this article! Today we have seen Paperclip, an attachment management solution for Rails, in action and discussed its main concepts. There is much more to this gem, so be sure to view its documentation.

Also, I recommend visiting Paperclip’s wiki page as it presents a list of “how to” tutorials and a bunch of links to third-party gems supporting Azure and Cloudinary and allowing you to easily minify uploaded files.

Thank you for staying with me, and see you soon!