Rails File Uploading You Can Believe in with Shrine

Share this article

Rails File Uploading You Can Believe in with Shrine

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!

Frequently Asked Questions (FAQs) about Rails File Uploading with Shrine

What is Shrine and why is it used in Rails for file uploading?

Shrine is a versatile and flexible file attachment library for Ruby applications. It is used in Rails for file uploading due to its advanced and customizable features. Shrine provides support for direct uploads, background processing, and promotes a modular approach, allowing developers to choose only the components they need. It also supports various storage options, including local disk storage, cloud storage like Amazon S3, Google Cloud Storage, and more.

How does Shrine compare to other file uploading tools like CarrierWave and Paperclip?

Shrine offers more flexibility and customization options compared to other file uploading tools like CarrierWave and Paperclip. It supports a wider range of storage options and promotes a modular approach, allowing developers to pick and choose the components they need. Shrine also provides support for direct uploads and background processing, which can significantly improve the performance of your Rails application.

How can I implement direct uploads with Shrine in Rails?

Implementing direct uploads with Shrine in Rails involves setting up an endpoint for your application to receive the uploaded files, configuring Shrine to use this endpoint, and then using JavaScript on the client side to send the files to this endpoint. Shrine provides a comprehensive guide on how to set this up, including example code and explanations.

Can I use Shrine with Active Record in Rails?

Yes, Shrine is fully compatible with Active Record in Rails. It provides a plugin system that allows you to add functionality to your file attachments, including validations, processing, and more. Shrine also integrates seamlessly with Rails’ form helpers, making it easy to add file upload fields to your forms.

How can I handle file processing with Shrine in Rails?

Shrine provides support for background processing, which allows you to offload time-consuming tasks like image resizing or video transcoding to a separate process. This can significantly improve the performance of your Rails application. Shrine provides a comprehensive guide on how to set up background processing, including example code and explanations.

What storage options does Shrine support?

Shrine supports a wide range of storage options, including local disk storage, cloud storage like Amazon S3, Google Cloud Storage, and more. It also provides a plugin system that allows you to add support for additional storage options.

How can I add validations to my file uploads with Shrine?

Shrine provides a plugin system that allows you to add validations to your file uploads. This can be used to ensure that only files of a certain type, size, or other characteristics are uploaded to your application.

Can I use Shrine with other Ruby frameworks like Sinatra?

Yes, Shrine is not limited to Rails and can be used with any Ruby framework. It provides a comprehensive guide on how to set up Shrine with Sinatra, including example code and explanations.

How can I handle file deletion with Shrine?

Shrine provides support for file deletion, allowing you to remove files from storage when they are no longer needed. This can be done manually, or you can use Shrine’s background deletion feature to automatically remove files after a certain period of time.

How can I troubleshoot issues with Shrine in Rails?

Shrine provides detailed logging and error reporting features, making it easy to troubleshoot any issues that may arise. If you’re still having trouble, the Shrine community is very active and helpful, and there are numerous resources available online to assist you.

Kingsley SilasKingsley Silas
View Author

Kingsley Silas is a web developer from Nigeria. He has a hunger for acquiring new knowledge in every aspect he finds interesting.

file uploadGlennGRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week