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