Upload images to Amazon S3 with Carrierwave on Rails


While searching for ways to upload images to Amazon S3 on Rails, I stumbled upon two gems that simplifies this task, Paperclip and Carrierwave. I tried Paperclip first and made an article about it here. A senior colleague of mine recommended Carrierwave, so I tried to give it a shot and it was worth it. It is much cleaner to implement because it requires the user to create a helper class for uploading. The model just needs to mount the helper class. This separates the code for uploading unlike on Paperclip where the code for uploading is included in the model and requires the user to create a migration.

Setup Amazon S3

This article does not cover setting up an Amazon S3 bucket. This article assumes that you have already setup your Amazon S3 bucket and familiar with your AWS credentials. You should know your bucket's name, host name, AWS access id, AWS secret access key and AWS region.


Photos app

I'll demonstrate how to upload files to S3 with Carrierwave on Rails by building a simple photos app. You can download the source code here.

Create a new rails project by executing the command below.

rails new photos

Generate and migrate a Photo model with attributes "name" and "image" by executing the command below.

rails g model photo name:string image:string
rake db:migrate


Setup Figaro

Use the gem Figaro to make it easy to securely configure Rails applications.

Add figaro gem to your Gemfile.

gem 'figaro'

Install figaro.

bundle install
bundle exec figaro install

This will generate an application.yml file at the config directory. The file's content should look like the code below. Replace the value with your Amazon S3 bucket's credentials.

config/application.yml

S3_BUCKET_NAME: "mybucketname"
AWS_ACCESS_KEY_ID: "AKI84JDHFYRKW80Q43RQ"
AWS_SECRET_ACCESS_KEY: "HJD348asd3dgdj3ysdjshHDSJ39DSH393D"
AWS_REGION: "ap-southeast-1"

This file contains the credentials for your S3 bucket and shouldn't be added to your git repository, fortunately figaro adds this file to your .gitignore file upon installation. A good practice for using figaro is adding an application.yml.template file to your repository's config directory so that other users can just copy its contents when setting up their application.yml on their new development environment.

config/application.yml.template

S3_BUCKET_NAME: ""
AWS_ACCESS_KEY_ID: ""
AWS_SECRET_ACCESS_KEY: ""
AWS_REGION: ""


Setup Carrierwave

Add these lines to the Gemfile.

gem 'carrierwave'
gem 'mini_magick'
gem 'fog'

Carrierwave is a gem for uploading files. MiniMagick is a gem for resizing images and Fog::AWS is a gem that adds support for Amazon Web Services. Install these gems by executing the command below.

bundle install

Generate an image uploader by executing the command below.

rails g uploader image

Executing the command above generates an ImageUploader class at app/uploaders/image_uploader.rb

class ImageUploader < CarrierWave::Uploader::Base

  include CarrierWave::MiniMagick

  storage :fog

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  version :thumb do
    process :resize_to_fit => [950, 850]
  end

end

Carrierwave supports the gems MiniMagick and RMagick for photo resizing. In this case I chose to use MiniMagick because it is more lightweight compared to RMagick. Set fog as the storage option instead of file, because we are gonna use Amazon S3 for storage. The method stor_dir sets where the file should be uploaded.


Mount the image uploader on the Photo model.

app/models/photo.rb

class Photo < ActiveRecord::Base
  mount_uploader :image, ImageUploader
end


Configure Carrierwave to use S3 credentials and stop it from using SSL when accessing the bucket.

config/initializers/carrierwave.rb

CarrierWave.configure do |config|
  config.fog_credentials = {
      :provider               => 'AWS',
      :aws_access_key_id      => ENV['AWS_ACCESS_KEY_ID'],
      :aws_secret_access_key  => ENV['AWS_SECRET_ACCESS_KEY'],
      :region                 => ENV['AWS_REGION']
  }
  config.fog_directory  = ENV['S3_BUCKET_NAME']
  config.fog_use_ssl_for_aws = false
end


Controller and views

Create a Photo controller by executing the command below.

rails g controller photos

Modify the PhotosController to look like the code below.

app/controllers/photos_controller.rb
class PhotosController < ApplicationController
  before_action :set_photo, only: [:show, :edit, :update, :destroy]

  def index
    @photos = Photo.all
  end

  def show
  end

  def new
    @photo = Photo.new
  end

  def edit
  end

  def create
    @photo = Photo.new(photo_params)
    if @photo.save
      redirect_to @photo, notice: 'Photo was successfully created.'
    else
      render :new
    end
  end

  def update
    if @photo.update(photo_params)
      redirect_to @photo, notice: 'Photo was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @photo.destroy
    redirect_to photos_url, notice: 'Photo was successfully destroyed.'
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_photo
      @photo = Photo.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def photo_params
      params.require(:photo).permit(:name, :image)
    end
end

Create an index file for our homepage. Its content should look like the code below and take note of the photo.image.thumb. It is the reference to the resized version of the image.

app/views/photos/index.html.erb
<p id="notice"><%= notice %></p>
<h1>Listing Photos</h1>

<table>
  <thead>
    <tr>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @photos.each do |photo| %>
      <tr>
        <td><%= photo.name %></td>
        <td><%=  image_tag photo.image.thumb %></td>
        <td><%= link_to 'Show', photo %></td>
        <td><%= link_to 'Edit', edit_photo_path(photo) %></td>
        <td><%= link_to 'Destroy', photo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Photo', new_photo_path %>

Create a partial form for our new and edit actions. The file_field will be used for selecting a file to upload. Take note of :html => {:multipart => true}, this is necessary to upload the image in multiple chunks.

app/views/photos/_form.html.erb
<%= form_for(@photo,:html => {:multipart => true}) do |f| %>
  <% if @photo.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@photo.errors.count, "error") %> prohibited this photo from being saved:</h2>

      <ul>
      <% @photo.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <p>
    <%= f.label :name %><br/>
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.file_field :image %>
  </p>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Create our view for our show action. Notice that I use photo.image instead of photo.image.thumb like on our index page. This shows the original size of the uploaded.

app/views/photos/show.html.erb
<p id="notice"><%= notice %></p>
<td><%= @photo.name %></td>
<td><%=  image_tag @photo.image %></td>
<%= link_to 'Edit', edit_photo_path(@photo) %> |
<%= link_to 'Back', photos_path %>
app/views/photos/new.html.erb
<h1>New Photo</h1>
<%= render 'form' %>
<%= link_to 'Back', photos_path %>
app/views/photos/edit.html.erb
<h1>Editing Photo</h1>
<%= render 'form' %>
<%= link_to 'Show', @photo %> |
<%= link_to 'Back', photos_path %>

Make sure we have configured our routes properly.

config/routes.rb
Rails.application.routes.draw do
  resources :photos
end

The photos app is finished. Execute the code below.

rails s

Go to http://localhost:3000/photos and see the rails photo app work. Happy coding!