Ruby
Article
By Kingsley Silas

Rails File Uploading You Can Believe in with Shrine

By Kingsley Silas

In my previous article we learned how we to enable file upload in Rails using Refile. Today we will look at another file uploading gem – Shrine. Shrine is a another toolkit for file uploads in Ruby applications.

In its introductory blog post, the author of Shrine, Janko Marohnić, indicates that Shrine is heavily influenced by both Refile and CarrierWave. Shrine makes use of a plugin system for everything, shipping with tons of plugins, some of which I will make use of today.

We will be using Shrine to build a Rails application that stores images and books. Yeah books…you might want to show your friends your latest collection of books. I hope you love to read as much as I do. Let’s build this thing and put it out for the world to see.

ImageMagick Installation

For Shrine to work you need ImageMagick installed. Depending on your operating system, use one of the steps below.

Mac Users:

brew install imagemagick

Ubuntu Users:

sudo apt-get install imagemagick

If neither of the above works for you, meaning you are on Windows or something else, check the installations page on the ImageMagick site. There are binary releases for Windows 7 and above.

Rails Application Generation

Run the command to create your new application:

rails new book-showcase

We are going to be using a single controller

rails generate controller Books

Set up the corresponding routes:

config/routes.rb



Rails.application.routes.draw do
  resources :books

  root to: "books#index"
end

Integrate Shrine

Before we proceed to creating a model, let’s pull Shrine into the Rails app. Open up your Gemfile and add the Shrine gem and it’s dependencies.

Gemfile



# Shrine Dependencies
gem 'fastimage'
gem 'image_processing'
gem 'mini_magick'
gem 'shrine'
  • fastimage is used to extract the image dimensions.
  • image_processing includes some helpers for using ImageMagick
  • mini_magick is a low-memory replacement for RMagick, a Ruby wrapper around ImageMagick

Run

bundle install

Now let’s create our Book model:

rails generate model Book

Open your migration and make it look like this:

xxx_create_books.rb



class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books do |t|
      t.string :name
      t.text :image_data

      t.timestamps null: false
    end
  end
end

Run the migration

rake db:migrate

We need to configure the model with Shrine functionality. Create a new file in your model folder, call it image_uploader.rb:

touch app/models/image_uploader.rb

Paste the following inside the file you just created:

app/models/imageuploader.rb_

class ImageUploader < Shrine
  include ImageProcessing::MiniMagick

  plugin :activerecord
  plugin :determine_mime_type
  plugin :logging, logger: Rails.logger
  plugin :remove_attachment
  plugin :store_dimensions
  plugin :validation_helpers
  plugin :versions, names: [:original, :thumb]

  Attacher.validate do
    validate_max_size 2.megabytes, message: 'is too large (max is 2 MB)'
    validate_mime_type_inclusion ['image/jpg', 'image/jpeg', 'image/png', 'image/gif']
  end

  def process(io, context)
    case context[:phase]
    when :store
      thumb = resize_to_limit!(io.download, 200, 200)
      { original: io, thumb: thumb }
    end
  end
end

As mentioned, Shrine provides us with different plugins for different functions. Here are the plugins included in our uploader and their functions:

  • :activerecord: This extends the “attachment” interface with support for ActiveRecord. Whenever an “attachment” module is included, additional callbacks are added to the model.
  • :determine_mime_type: This stores the actual MIME type of the uploaded file.
  • :logging: The logging plugin logs any storing/processing/deleting that is performed. By passing in Rails.logger to the :logger option, we change the logger to be useful in our Rails application.
  • :remove_attachment: The remove_attachment plugin allows you to delete attachments through checkboxes on the web form.
  • :store_dimensions: This plugin extracts and stores dimensions of the uploaded image.
  • :validation_helpers: This provides helper methods for validating attached files. Take a look at the following Attacher block:

app/models/imageuploader.rb_

Attacher.validate do
  validate_max_size 2.megabytes, message: 'is too large (max is 2 MB)'
  validate_mime_type_inclusion ['image/jpg', 'image/jpeg', 'image/png', 'image/gif']
end

The validators are pretty straightforward. Here, we are constraining the size to 2MB or less and the acceptable file types to jpg, png, and gif.

  • :versions: The versions plugin enables your uploader to deal with versions of an image. To generate versions, you simply return a hash of versions like we did in our uploader.

app/models/imageuploader.rb_

plugin :versions, names: [:original, :thumb]

Now, in the process method, handle any version (other than :original) and return it. As you can see, we’re doing this for the store phase, meaning, when the file is being stored:

...

def process(io, context)
  case context[:phase]
  when :store
    thumb = resize_to_limit!(io.download, 200, 200)
    { original: io, thumb: thumb }
  end
end

Setup your BooksController:

app/controllers/bookscontroller.rb_

class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

  def index
    @books = Book.all
  end

  def show
  end

  def new
    @book = Book.new
  end

  def edit
  end

  def create
    @book = Book.new(book_params)

    if @book.save
      redirect_to @book, notice: 'book was successfully created.'
    else
      render :new
    end
  end

  def update
    if @book.update(book_params)
      redirect_to @book, notice: 'book was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @book.destroy
    redirect_to books_url, notice: 'book was successfully destroyed.'
  end

  private

  def set_book
    @book = Book.find(params[:id])
  end

  def book_params
    params.require(:book).permit(:name, :image, :remove_image)
  end
end

You need to create an initializer for Shrine:

touch config/initializers/shrine.rb

Paste in the following:

config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"
require "image_processing/mini_magick"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
}

Shrine ships with two (2) default storages: FileSystem and S3.

FileSystem

FileSystem storage deals with uploads to the file system of the application itself. It gets initialized with a base folder and a prefix, like we have above. The prefix is a directory relative to the base folder where files will be stored, and it gets included in the URL.

S3

S3 storage is responsible for uploads to the Amazon Web Service (AWS) S3 service. To use S3 storage, you will need the aws-sdk gem in your Gemfile. Do not forget to bundle install.

Now go over to config/initilazers/shrine.rb to set things up for S3 storage.

config/initializers/shrine.rb

require "shrine"
require "shrine/storage/s3"

s3_options = {
  access_key_id:     ENV.fetch("S3_ACCESS_KEY_ID"),
  secret_access_key: ENV.fetch("S3_SECRET_ACCESS_KEY"),
  region:            ENV.fetch("S3_REGION"),
  bucket:            ENV.fetch("S3_BUCKET"),
}

Shrine.storages = {
  cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
  store: Shrine::Storage::S3.new(prefix: "store", **s3_options),
}

At first we require shrine and shrine s3. S3 prefix option is passed the name of the bucket where our files will be uploaded. In the example we have above, there will be two (2) buckets; one for store, the other for cache. The next option, s3-options, gives us access to AWS. You will need to get your AWS access key and secret access key and put them into the environment variables.

Now let’s work on the views. The view to hold our form:

app/views/books/form.html.erb_

<%= form_for(@book) do |f| %>
  <%= render "error_messages", target: @book %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :image %><br>
    <%= f.file_field :image %>
  </div>
  <%- if @book.image_data? %>
    <div class="field">
      <%= image_tag @book.image_url(:thumb) %>
    </div>
    <div class="field">
      Remove attachment: <%= f.check_box :remove_image %>
    </div>
  <%- end %>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

I want us to separate the error messages, that is a new habit I have recently developed. Create a folder in your views named application. Create a partial calledj _error_messages.html.erb and paste in the error messages:

app/views/application/errormessages.html.erb

<% if target.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@book.errors.count, "error") %> prohibited this book from being saved:</h2>

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

The form is really simple. Just use the form.file_field :image to render the appropriate field.

Let’s work on the show page. At this point, when we try to submit the form we’ll get an Action Controller: Exception caught message because of the missing show template.

This is how the view code for your show page should look.

app/views/books/show.html.erb

<p id="notice"><%= notice %></p>

<div>
  <strong>Name:</strong>
  <%= @book.name %>
</div>
<div>
  <strong>Image:</strong>
  <%= image_tag @book.image_url(:original) %>
</div>

<%= link_to 'Edit', edit_book_path(@book) %> |
<%= link_to 'Back', books_path %>

Let’s setup our Edit page too:

app/views/books/html.erb

<h1>Editing Book</h1>

<%= render 'form' %>

<%= link_to 'Show', @book %> |
<%= link_to 'Back', books_path %>

And the index page:

app/views/books/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Listing Books</h1>

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

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

<br>

<%= link_to 'New Book', new_book_path %>

Versions

In your Rails application, you may want to store a thumbnails (or other versions) alongside the original image. For that you need to load the versions plugin, which I mentioned above. We did that already in the image_uploader.rb file. Now, just have the #process method return a Hash of versions:

app/views/models/imageuploader.rb_

class ImageUploader < Shrine
  include ImageProcessing::MiniMagick

  plugin :activerecord
  plugin :determine_mime_type
  plugin :logging, logger: Rails.logger
  plugin :remove_attachment
  plugin :store_dimensions
  plugin :validation_helpers
  plugin :versions, names: [:original, :large, :medium, :small, :thumb]

  Attacher.validate do
    validate_max_size 2.megabytes, message: 'is too large (max is 2 MB)'
    validate_mime_type_inclusion ['image/jpg', 'image/jpeg', 'image/png', 'image/gif']
  end

  def process(io, context)
    if context[:phase] == :store
      size_700 = resize_to_limit!(io.download, 700, 700)
      size_500 = resize_to_limit(size_700,    500, 500)
      size_300 = resize_to_limit(size_500,    300, 300)
      thumb = resize_to_limit(size_300, 200, 200)

      { original: io, large: size_700, medium: size_500, small: size_300, thumb: thumb }
    end
  end
end

You can pick which of the versions you want to render in the view, just like we did with the view for index. Open your index view and change the rendered image to the small version.

app/views/books/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Listing Books</h1>

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

  <tbody>
    <% @books.each do |book| %>
      <tr>
        <td><%= book.name %></td>
        <td><%= image_tag book.image_url(:small) %></td>
        <td><%= link_to 'Show', book %></td>
        <td><%= link_to 'Edit', edit_book_path(book) %></td>
        <td><%= link_to 'Destroy', book, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Book', new_book_path %>

Conclusion

In this article we learned how to enable image uploading in our Rails application using Shrine. You can checkout Shrine’s page on Github if you want to try out some exciting features not mentioned here. Shrine has some great support for putting file uploads into background jobs or direct uploads, just to name a couple of features we didn’t cover today. Also, Shrine is not just for Rails, you can use it with any Ruby application.

Thanks for reading and see you soon!

  • Saint

    When creating a new book I get this error: ActiveRecord::UnknownAttributeError in BooksController#create unknown attribute ‘image’ for Book.

  • Dan Lawler

    Good stuff man. Thanks for sharing.

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.