Rails Image Management with Paperclip and Amazon S3

  • rails, gems, tutorials
  • 2 Comments

If you've ever wondered how to combine Rails, the Paperclip gem, and Amazon S3 for image uploads, processing, and storage, look no further.

I recently deployed a simple Rails app called To Taste for my friends and family to share recipes we were always trying to pass around and preserve through emails or handwritten recipe cards. One of the main features I wanted was for users to be able to upload pictures of the cooked dishes. I also wanted the images to be available in different sizes and I didn't want image uploading or storage to strain app performance.

To achieve these three objectives, I decided to use Paperclip and Amazon S3 based on the recommendation of Heroku, where I planned to deploy the app. Paperclip is an easy-to-use file attachment library that treats uploads as model attributes, while Amazon S3 provides off-site image storage that reduces the load on Heroku, making your app faster.

The first thing I need to do is sign up for Amazon S3 and set my security credentials in the dropdown menu link called, you guessed it, “Security Credentials." Here I can set my access key ID and secret access key that will connect my Rails app to the Amazon S3 “bucket" where my images are stored. Clicking the “Create Bucket" button provides detailed instructions for creating and naming your “bucket."

Add Dependencies

Back in my Rails app I need to add the Paperclip, Amazon AWS, and Figaro gems to my Gemfile and then bundle install:

gem 'aws-sdk', '< 2.0'
gem 'paperclip', '~> 4.2'
gem 'figaro'

Figaro makes it easy to manage my credentials for services like Amazon S3. It creates a config/application.yml file where I can store my passwords and adds the file to .gitignore so it is never pushed to Github. All I need to do in config/application.yml is put in the following key/value pairs:

AWS_ACCESS_KEY_ID: my_access_key_id
AWS_SECRET_ACCESS_KEY: my_secret_access_key
AWS_S3_REGION: us-east-1
AWS_S3_BUCKET: the_name_of_my_bucket

Configure Rails and Paperclip with Amazon S3

Next, I have to add the Amazon S3 credentials to the production environment, located in app/config/environments/production.rb:

config.paperclip_defaults = {
  storage: :s3,
  s3_credentials: {
    bucket: ENV['AWS_S3_BUCKET'],
    access_key_id: ENV['AWS_ACCESS_KEY_ID'],
    secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
  }
}

If you want to test this in development, just add the above code to app/config/environments/development.rb too.

Setting up Paperclip

Now, on to Paperclip. The first step is to install an image resizing processor called ImageMagick by running brew install imagemagick. This requires an OS X package manager called Homebrew.

Running which convert displays where ImageMagick is installed, and in my case it's in /usr/local/bin/convert. I need to add this location to config/environments/development.rb) with the following code:

Paperclip.options[:command_path] = "/usr/local/bin/"

I am using a form_for helper to create and update recipes in the New and Edit views, so I need to add { :multipart => true } to the :html hash to enable file attachments in the form, while <%= f.file_field :image %> returns a file upload input tag:

<%= form_for :recipe, :url => recipes_path, :html => { :multipart => true } do |f| %> 
  <div class="field">
    <%= f.label :image %>
    <%= f.file_field :image %>
  </div>
  <div class="field">
    <%= f.label :ingredients %>
    <%= f.text_area :ingredients %>
  </div>
  <div class="field">
    <%= f.label :instructions %>
    <%= f.text_area :instructions %>
  </div>
  <div class="actions">
    <%= f.submit "Create Recipe" %>
  </div>
<% end %>

Define the Image Attribute in the Model

To attach an image to the Recipe model, Paperclip has a helper method called has_attached_file and I just need to add the :image symbol. The has_attached_file also accepts a :styles hash that sets the resized image options. Finally, the validates_attachment method allows me to restrict image uploads to specific formats (good for security) and limit their size (great for storage considerations).

class Recipe < ActiveRecord::Base
  has_attached_file :image, :styles => { :small => "100x100>", :med => "200x200>", :large => "514x386>" }, :default_url => ":style/fallback-recipe-img.png"
  validates_attachment :image, :content_type => { :content_type => ["image/jpeg", "image/gif", "image/png"] }, :size => { :in => 0..3.megabytes }
end

Database Migration Needed

To add the :image attribute to the Recipes table schema, I need to run a migration...

rails generate migration AddImageToRecipes

...and in the migration file I can use Paperclip's add_attachment and remove_attachment helper methods:

class AddImageToRecipes < ActiveRecord::Migration
  def self.up
    add_attachment :recipes, :image
  end

  def self.down
    remove_attachment :recipes, :image
  end
end

I run rake db:migrate and restart the server.

This migration creates the following columns in the Recipes table: image_file_name, image_content_type, image_file_size, and image_updated_at.

Update the Controller

Since To Taste is a Rails 4 app and I'm using strong parameters, I need to update my recipe_params private method to include the :image parameter in the whitelisted params.

def create
  @recipe = Recipe.new(recipe_params)
  @recipe.user_id = current_user.id
  if @recipe.save
    redirect_to @recipe, notice: "Success! You've added a new recipe."
  else
    render 'new'
  end
end

def update
  @recipe = Recipe.find(params[:id])
  if @recipe.update(recipe_params)
    redirect_to @recipe
  else
    render 'edit'   
  end
end

private
  def recipe_params
    params.require(:recipe).permit(:ingredients, :instructions, :image)
  end

Displaying Images in the Views

Images uploaded with Paperclip are now stored in Amazon S3, but metadata such as the image name and location in S3 are stored in the database. All that's left is to call an image in a view through the .url method that provides access to the image's url on Amazon (http://your_bucket_name.s3.amazonaws.com/...) and specify one of the styles I included in has_attached_file method.

<%= image_tag recipe.image.url(:med) %>

Images are now being served from Amazon S3 and they won't interfere with other requests, allowing pages to load quicker than if they were served directly from the app itself.