Uploading many images to Cloudinary with Carrierwave

We needed a way to allow users to upload multiple images per "page". Initially S3 was considered due to previous experience but upon finding Cloudinary this seemed to offer better functionality with image manipulation included and a freemium pricing model. Carrierwave was then chosen due to how Cloudinary has integrated itself with it.

Before even starting with Cloudinary and Carrierwave we need to make sure the models and migrations are setup so we can store everything in the database. We'll assume that we have a model and migration already setup to create "contents" table to store items of content / pages. We then need to add an images table, for this we have the following migration.

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.integer :content_id
      t.string :file

      t.timestamps
    end
  end
end

This migration creates in images table with a content_id field for storing references to content items and a file field to store the reference to the Cloudinary file, finally a timestamp is added as Rails standards.

As for the models, the Content module needs to be told that it can have many images, this is done by using the has_many association.

class Content < ActiveRecord::Base
  attr_accessible :body, :content_type, :images_attributes, :published, :slug, :title
  has_many :images, :dependent => :destroy
  accepts_nested_attributes_for :images, :allow_destroy => true

end

In the above example from SimpleSite you will see :images_attributes has been added to attr_accessible so we can mass assign the images in the POST. The has_many features :dependent => :destroy so that all images are deleted if their related content is. accepts_nested_attributes_for then allows for the images_attributes has to be accepted and by setting allow_destroy it allows us to delete images by passing _destroy => 1 into the post, this will be explained a little more later when we create the form.

The image model contains the reverse side of the has_many with belongs_to :content:

class Image < ActiveRecord::Base
  attr_accessible :content_id, :file
  belongs_to :content
  mount_uploader :file, ImageUploader
end

The uploader is also added with mount_uploader, this will make a little more sense shortly when we create the uploader.

We are now ready to get started with Cloudinary and Carrierwave, first by adding the gems to the Gemfile (make sure you run bundle install after updating the Gemfile):

gem 'carrierwave'
gem 'cloudinary'

When using Cloudinary on Heroku the CLOUDINARY_URL environment variable will be added upon enabling the addon. If you're not using Heroku there are a number of ways to set the Cloudinary configuration.

Run the command rails g uploader ImageUploader to create the ImageUploader uploader in /app/uploaders/image_uploader.rb. This will come with some default code and comments from Carrierwave, but one important line to add is include Cloudinary::CarrierWave. The image_uploader.rb file for SimpleSite is:

class ImageUploader < CarrierWave::Uploader::Base
  include Cloudinary::CarrierWave

  def cache_dir
    "#{Rails.root}/tmp/uploads"
  end

  process :convert => 'png'

  version :square do
    cloudinary_transformation :width => 200, :height => 200, :crop => :fill, :gravity => :face
  end

  version :slideshow do
    cloudinary_transformation :width => 1170, :height => 400, :crop => :fill
  end

  version :thumb do
    resize_to_fit 170, 120
  end
end

From this example you will see the Cloudinary include is added at the top of the calls. This is then followed by the setting of the cache directory, because Heroku has a read-only file system /public can't be used to cache the images, therefore /tmp needs to be used instead. The uploader then includes settings for the output of images, all images are converted to png, there are then three different types of image output, square, slideshow and thumb, these different sizes and settings to be preset.

The SimpleSite platform creates a few content items via a rake command then only allows users to update rather than edit them. So here I am going to focus on the edit and update method from the contents controller.

def edit
  @content = Content.find_by_slug(params[:id])
  not_found and return if !@content
  @content.images.build
  render :new
end

The edit method finds the content based on the slug in the URL, if there it no content it loads a custom not_found method, otherwise it builds a content image and renders the new method (new and edit both share the same view)

def update
  content = Content.find_by_slug(params[:id])
  not_found and return if !content
  content.update_attributes(params[:content])
  redirect_to content_path
end

The update method again finds the content based on the slug in the URL and loads the not_found method if needed. It then goes on to use the update.attributes method to save the content, and because how we created models this saves all of the images and uploads them to Cloudinary too. Finally the user is redirected to the content.

Lastly the view new.html.erb which is used as the form for both the new and edit methods. <%= form_for @content do |f| %> is used to correctly generate the form tag based on the content being edited. Then for the images <%= f.fields_for :images, @content.images do |images_field| %> is used in which the current image is displayed or a file upload field is displayed based on <% if images_field.object.new_record? %>. When an image already exists it's displayed as a thumbnail using the "thumb" preset defined in the uploader <%= image_tag(images_field.object.file.url(:thumb)) %>, a checkbox is also added <%= images_field.check_box :_destroy %> named _destroy, and if this is passed through as checked the selected image is deleted.

<div class="row">
  <div class="span2">
    <%= render "/admin_menu" %>
  </div>
  <div class="span10">
    <h1><%= @content.title ? "Edit - #{@content.title}" : "New page" %></h1>
    <%= form_for @content do |f| %>
    <%= f.label :title, "Page title:" %>
    <%= f.text_field :title, :class => "span8" %>
    <%= f.label :body, "Body text:" %>
    <%= f.text_area :body, :class => "span8" %>
    <span class="help-block">Enter body text in <a href="http://en.wikipedia.org/wiki/Markdown">Markdown</a> format</span>
    <hr/>
    <ul class="thumbnails">
      <%= f.fields_for :images, @content.images do |images_field| %>
      <% if images_field.object.new_record? %>
      <li id="<%= images_field.object_name.gsub(/[^0-9]+/,'') %>" class="span4">
        <%= images_field.label :file, "Image:" %>
        <%= images_field.file_field :file %>
      </li>
      <% else %>
      <li id="<%= images_field.object_name.gsub(/[^0-9]+/,'') %>" class="span2">
        <div class="thumbnail">
          <%= image_tag(images_field.object.file.url(:thumb)) %>
          <div class="caption">
            <%= images_field.check_box :_destroy %>
            <%= images_field.label(:_destroy, "Delete image", :class => "checkbox inline") %>
          </div>
        </div>
      </li>
      <% end %>
      <% end %>
    </ul>
    <span class="help-block">When one image is added it will be shown on the left of the body text, when more are added it will create a slideshow.</span>
    <div class="form-actions">
      <%= f.submit "Save", :class => "btn btn-primary" %>
    </div>
    <% end %>
  </div>
</div>

A nice to have here is to be able to add more than one image. When the file field is added they have an id within the name, by using javascript the field can be duplicated and the id incremented, then Rails will see this as another image allowing it to be uploaded and added.

!function ($) {
  $(function(){
    var $form = $('form.edit_content');
    var $list = $form.find('ul');

    var $btn = $('<button type="button" class="btn">Add another image</button>');

    $btn.on('click', function (e) {
      var $lis = $list.find('li');
      var newIndex = $lis.length;

      function updateNumber (index, value) {
        return value.replace(/(\d+)/, newIndex);
      }

      var $newLi = $lis.last().clone();
      $newLi.find('label').attr('for', updateNumber);
      $newLi.find('input')
        .attr('id', updateNumber);
        .attr('name', updateNumber);

      $list.append($newLi);
    });  
  });
}(window.jQuery);

Please get in contact if you're looking for further help in setting up integration with Cloudinary, whether you're using Carrierwave or not.

Simple SEO PDF guide

Get our latest PDF guide, Simple SEO.

Twitter