Last Updated:

Loading with guides and paper clips

This is the last article in the Booting from Rails series. In the last couple of months, we've already discussed shrine, Dragonfly, and Carrierwave jewels. Today's guest is Paperclip from Thoughtbot, the company that manages gemstones like FactoryGirl and Bourbon.

Paperclip is probably the most popular attachment management solution for Rails (over 13 million downloads), and for good reason: it has a lot of features, a large community, and detailed documentation. Hope you want to know more about this gemstone!

In this article, you'll learn how to:

  • Prepare to install the paperclip
  • Integrating Paperclip into a Rails Application
  • Add scan attachments
  • Thumbnail generation and image processing
  • Obfuscate URLs
  • Store attachments on Amazon S3
  • Secure files in the cloud with authorization logic

The source code for this article is available on GitHub.

Before we dive into the code, let's first discuss some caveats you need to be aware of in order to work successfully 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 computer (it is available for all major platforms), and Paperclip must have access to it.
  • The file command must be accessible from the command line. For Windows, it's available through the Development Kit, so follow these instructions if you don't already have DevKit installed.

When you're ready, go ahead and create a new Rails application (I'll use Rails 5.0.2) without the default test suite:

rails new UploadingWithPaperclip -T

Insert a gemstone into a paperclip:

gem “paperclip”, “~> 5.1”

Install this:

bundle install

Suppose we create an application for bookshelves that represents a list of books. Each book will have a title, description, author's name, and cover image. To get started, create and apply the following migration:

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

Pay attention to the attachment type that Paperclip is presented for us. Under the hood, he's going to create four fields for us:

  • image_file_name
  • image_file_size
  • image_content_type
  • image_updated_at

Unlike the Shrine and Carrierwave gemstones, Paperclip does not have a separate configuration file. All settings are defined within the model itself with the has_attached_file of the has_attached_file method, so add it now:

has_attached_file :image

Before moving on to the main part, let's also create a controller along with some views and routes.

Our controller will be very simple:

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 the index representation and partial:

<h1>Bookshelf</h1>
 
<%= link_to ‘Add book’, new_book_path %>
<ul>
  <%= render @books %>
</ul>
<li>
  <strong><%= link_to book.title, book_path(book) %></strong> by <%= book.author %>
</li>

Now the routes:

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

Nice! Now let's move on to the main section and encode the new action and form.

In general, uploading with Paperclip is easy. You only need to resolve the corresponding attribute (in our case, this is the image attribute, and we have already resolved it) and represent the form field in your form. Let's do it now:

<h1>Add book</h1>
 
<%= render ‘form’, book: @book %>
<%= 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 setting, you can already start downloading, but it's also a good idea to enter some checks.

Validations in Paperclip can be written using older helpers such as validates_attachment_presence and validates_attachment_content_type or using the validates_attachment method to define multiple rules at once. Let's stick to the last option:

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 less than 1 megabyte in size. Note that if the validation fails, no post-processing will be performed. Paperclip already has some error messages set for English, but if you want to support other languages, include the paperclip-i18n gem in your Gemfile.

Another important thing worth mentioning is that Paperclip requires you to check the content type or file name of all attachments, otherwise it will cause an error. If you are 100% sure that you do not need such checks (which is a rare case), use do_not_validate_attachment_file_type to explicitly do_not_validate_attachment_file_type which fields should not be checked.

After adding checks, let's also display the error messages in our form:

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

 

So, now the uploaded images should be displayed somehow. This is done using the image_tag Helper and the url method. Create Show View :

<h1><%= @book.title %> by <%= @book.author %></h1>
 
<%= image_tag(@book.image.url) if @book.image.exists?
 
<p><%= @book.description %></p>

We only display an image if it actually exists on disk. What's more, if you're using cloud storage, Paperclip will make a network request and check for the presence of the file. Of course, this operation may take some time, so what could you use present? or file? instead: they simply image_file_name whether the field image_file_name is filled with any content.

By default, all attachments are stored in a public/system folder, so you'll probably want to exclude it from the version control system:

public/system

However, displaying the full URI for a file may not always be a good idea, and you may need to hide it in some way. The easiest way to enable obfuscation is to provide two parameters has_attached_file method:

url: “/system/:hash.:extension”,
hash_secret: “longSecretString”

The proper values will be automatically inserted into the urlhash_secret is a required field, and the easiest way to create it is to use:

rails secret

In many cases, it is preferable to display a thumbnail image with some predefined width and height to preserve bandwidth. Paperclip solves this problem with styles: Each style has a name and a set of rules such as size, format, quality, etc.

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

has_attached_file :image,
                   styles: {
                       thumb: [“300×300#”, :jpeg],
                       original: [:jpeg]
                   }

# – geometry setting, meaning: "Crop if necessary, maintaining the aspect ratio."

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

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

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

<%= 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 any reason you want to update uploaded images manually, you can use the following commands to update only thumbnails, add missing styles, or update all images:

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

Like all similar solutions, Paperclip allows you to upload files to the cloud. It has built-in support for S3 and Fog adapters, but there are also third-party gems for Azure and Dropbox. In this section, I'll show you how to integrate Paperclip with Amazon S3. First, add the aws-sdk gem:

gem ‘aws-sdk’

Install this:

bundle install

Then provide a new set of parameters for the has_attached_file method:

has_attached_file :image,
                   styles: {
                       thumb: [“300×300#”, :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 stick to the gemstone dotenv-rails for setting environment variables. You can provide all values directly within the model, but not make it public.

Interestingly, s3_credentials also accepts the path to the YAML file containing your keys and the name of the s3_credentials. In addition, you can set different values for different environments, for example:

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 in your S3 bucket.

Let's say you don't want your downloaded files to be available to everyone. By default, all uploads to the cloud are marked as public, which means anyone can open a file via a direct link. If you want to enter some authorization logic and check who can view the file, set the s3_permissions parameter to :p rivate for example:

has_attached_file :image,
                   styles: {
                       thumb: [“300×300#”, :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

However, now no one but you will be able to see the files. So let's create a new download action for BooksController:

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 expose any authorization logic using gems like CanCanCan or Pundit.

Don't forget to set the participant's itinerary:

resources :books do
   member do
     get ‘download’
   end
 end

The helper should be used as follows:

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

We've come to the end of this article! Today we saw Paperclip, an investment management solution for Rails, and discussed its core concepts. This gemstone has much more, so be sure to check out its documentation.

Also, I recommend visiting the Paperclip wiki page because it provides a list of "how to" tutorials and several links to third-party gems that support Azure and Cloudinary and make it easy to minimize downloads.