The Talent500 Blog
Uploading Files With Rails and Shrine 1

Uploading Files With Rails and Shrine

There are several file uploading gems available, like CarrierWave, Paperclip, and Dragonfly, to mention a few. They all have unique characteristics, and you’ve probably used at least one of them.

Today, though, I’d like to offer Janko Marohni’s Shrine, a relatively new yet extremely creative approach. Unlike other comparable gems, it takes a modular approach, which means that each feature is packaged as a module (or plugin in Shrine’s nomenclature). Do you want help with validations? Install a plugin. Do you want some file processing done? Include a plugin! This method is fantastic since it allows you to simply select which features will be offered for whatever model.

Integrating Shrine

To begin, create a new Rails application that does not include the default testing suite:

 rails new FileGuru -T

For this demonstration, we will be  using Rails 5, however the majority of the ideas apply to versions 3 and 4.

Drop the Shrine gem into your Gemfile:

 gem “shrine”

Now run:

bundle install

Now we’ll need a model, which I’ll refer to as Photo. Shrine saves all file-related information in a special text column with the suffix _data. Create and apply the appropriate migration:

 rails g model Photo title:string image_data:text
 rails db:migrate


It should be noted that for previous Rails versions, the latter command should be:

 rake db:migrate

Shrine configuration parameters may be configured both globally and per-model. Of course, global settings are made within the initializer file. I’ll connect the essential files and plugins there. Shrine uses plugins to divide portions of functionality into independent modules, allowing you complete control over all accessible capabilities. There are plugins for validation, image processing, attachment caching, and more.

For the time being, let’s install two plugins: one to support ActiveRecord and another to configure logging. They will be implemented universally. Set up file system storage as well:
config/initializers/shrine.rb

1. require “shrine”
2. require “shrine/storage/file_system”
3.
4. Shrine.plugin :activerecord
5. Shrine.plugin :logging, logger: Rails.logger
6.
7. Shrine.storages = {
8. cache: Shrine::Storage::FileSystem.new(“public”, prefix: “uploads/cache”),
9. store: Shrine::Storage::FileSystem.new(“public”, prefix: “uploads/store”),
10 }


Logger will just provide some debugging information via the console, such as how long it took to process a file. This can be useful.

1 2015-10-09T20:06:06.676Z #25602: STORE[cache] ImageUploader[:avatar] User[29543] 1 file (0.1s)
2 2015-10-09T20:06:06.854Z #25602: PROCESS[store]: ImageUploader[:avatar] User[29543] 1-3 files (0.22s)
3 2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)

All files uploaded will be saved in the public/uploads directory. I don’t want Git to monitor these files, therefore remove this folder:

.gitignore
1.public/uploads


Create a new “uploader” class that will hold model-specific parameters. For the time being, this class will be empty:

models/image_uploader.rb
1. class ImageUploader < Shrine
2. End


Finally, add the following class to the Photo model:

models/photo.rb
1. include ImageUploader[:image]


[:image] adds a virtual attribute to be utilised when building a form. The above sentence may be worded as follows:

1. include ImageUploader.attachment(:image)
2. # or
3. include ImageUploader::Attachment.new(:image)

Nice! Now that the model has Shrine functionality, we can go on to the next phase.


Controller, Views, and Routes


For the sake of this demonstration, we will just need one controller to manage images. The index page will be the starting point:

Pages_controller.rb
1 class PhotosController < ApplicationController
2 def index
3 @photos = Photo.all
4 end
5 end

The view:
views/photos/index.html.erb
1 <h1>Photos</h1>
2
3 <%= link_to ‘Add Photo’, new_photo_path %>
4
5 <%= render @photos %>


A partial is required to render the @photos array:

views/photos/_photo.html.erb
1 <div>
2 <% if photo.image_data? %>
3   <%= image_tag photo.image_url %>
4 <% end %>
5 <p><%= photo.title %> | <%= link_to ‘Edit’, edit_photo_path(photo) %></p>
6 </div>


ActiveRecord’s image data? method determines if a record has an image.

image url is a Shrine method that just returns the original image’s path. Of course, displaying a small thumbnail is preferable, but we’ll get to that later.

Add all the necessary routes:

config/routes.rb
1 resources :photos, only: [:new, :create, :index, :edit, :update]
2
3 root ‘photos#index


Uploading Files


In this part, I’ll teach you how to add the ability to upload files. The controller’s operations are straightforward:



Photos_controller.rb
1 def new
2   @photo = Photo.new
3 End
4
5 def create
6   @photo = Photo.new(photo_params)
7   if @photo.save
8       flash[:success] = ‘Photo added!’
9       redirect_to photos_path
10   Else
11   render ‘new’
12   End
13 end


The only catch is that for strong parameters, you must allow the image virtual attribute rather than the image data.

Photos_controller.rb
1 private
2
3 def photo_params
4 params.require(:photo).permit(:title, :image)
5 end


Create a new view:
views/photos/new.html.erb
1 <h1>Add photo</h1>
2
3 <%= render ‘form’ %>


The partial form is also simple:
views/photos/_form.html.erb
1 <%= form_for @photo do |f| %>
2 <%= render “shared/errors”, object: @photo %>
3
4 <%= f.label :title %>
5 <%= f.text_field :title %>
6
7 <%= f.label :image %>
8 <%= f.file_field :image %>
9
10 <%= f.submit %>
11 <% end %


Please keep in mind that we are utilising the image attribute rather than the image data.
Finally, add another subpart to show errors:
views/shared/_errors.html.erb
1 <% if object.errors.any? %>
2 <h3>The following errors were found:</h3>
3
4 <ul>
5   <% object.errors.full_messages.each do |message| %>
6     <li><%= message %></li>
7   <% end %>
8 </ul>
9 <% end %>

This is pretty much it—you can begin uploading photographs immediately.

Validations

Of course, much more work is required to complete the example software. The main issue is that users can upload absolutely any type of file of any size, which is not ideal. As a result, another plugin is required to provide validations:

config/inititalizers/shrine.rb

1 Shrine.plugin :validation_helpers

Configure the ImageUploader’s validation logic:

models/image_uploader.rb
1 Attacher.validate do
2   validate_max_size 1.megabyte, message: “is too large (max is 1 MB)”
3   validate_mime_type_inclusion [‘image/jpg’, ‘image/jpeg’, ‘image/png’]
4 End

MIME Types

Another thing to keep in mind is that Shrine will detect the MIME type of a file by default by utilising the Content-Type HTTP header. This header is passed by the browser and is set only on the file extension, which is not always desired.

If you want to identify the MIME type based on the contents of a file, use the determine mime type plugin. I’ll put it within the uploader class because other models might not need it:
models/image_uploader.rb
1 plugin :determine_mime_type

By default, this plugin will use Linux’s file utilities.

Caching Attached Images

When a user sends a form with invalid data, the form is presented again with the errors shown above. The issue is that the associated picture will be lost, and the user will have to pick it again. This can easily remedied by installing a different plugin called cached attachment data:
models/image_uploader.rb
1 plugin :cached_attachment_data

Simply include a hidden field into your form.

views/photos/_form.html.erb
1 <%= f.hidden_field :image, value: @photo.cached_image_data %>
2 <%= f.label :image %
3 <%= f.file_field :image %>

Editing a Photo

Images may now be uploaded, however they cannot be edited, so let’s address that right immediately. The activities of the appropriate controller are rather simple:

Photos_controller.rb
1 def edit
2   @photo = Photo.find(params[:id])
3 End
4
5 def update
6   @photo = Photo.find(params[:id])
7   if @photo.update_attributes(photo_params)
8     flash[:success] = ‘Photo edited!’
9     redirect_to photos_path
10   Else
11     render ‘edit’
12   End
13 End


The following _form partial will be used:

views/photos/edit.html.erb
1 <h1>Edit Photo</h1>
2
3 <%= render ‘form’ %>


Users may still delete a posted image. To enable this, we’ll need—guess what—another plugin:

models/image_uploader.rb
1 plugin :remove_attachment

It makes use of a virtual attribute named remove image, thus allow it within the controller:

Photos_controller.rb
1 def photo_params
2   params.require(:photo).permit(:title, :image, :remove_image)
3 End


If a record includes an attachment, just offer a checkbox to delete it:

views/photos/_form.html.erb
1 <% if @photo.image_data? %>
2   Remove attachment: <%= f.check_box :remove_image %>
3 <% end %>

Generating a Thumbnail Image


We now show original photographs, which is not the greatest strategy for previews because they might be huge and take up too much space. Of course, you could just use the CSS width and height properties, but that’s also a horrible idea. You see, even if the image is styled to be tiny, the user will still need to download the actual file, which may be rather large.

As a result, it is far preferable to produce a little preview image on the server during the initial upload. This necessitates the use of two plugins as well as two extra gems. First, insert the gems:

1 gem “image_processing”
2 gem “mini_magick”, “>= 4.3.5”


The creator of Shrine designed Image processing as a custom gem. It provides several high-level image manipulation assist techniques. In turn, this gem is dependent on mini magick, a Ruby wrapper for ImageMagick. You’ll need ImageMagick on your PC to execute this example, as you might expect.

Install the following new gems:

1 Bundle install

Include the plugins and their dependencies now:

models/image_uploader.rb

1 require “image_processing/mini_magick”
2
3 class ImageUploader < Shrine
4   include ImageProcessing::MiniMagick
5   plugin :processing
6  plugin :versions
7 # other code…
8 end



The Processing plugin allows us to alter images (for example, shrink it, rotate, convert to another format, etc.). Versions, in turn, enable us to have one picture in several variations. Two versions of this demo will be saved: “original” and “thumb” (resized to 300×300).

Here is the code for processing a picture and storing two copies of it:

models/image_uploader.rb
1 class ImageUploader < Shrine
2 process(:store) do |io, context|
3 { original: io, thumb: resize_to_limit!(io.download, 300, 300) }
4 end
5 end


The image processing gem provides the function resize to limit!. If a picture is larger than 300×300, it simply compresses it to 300×300 and does nothing if it is less. It also retains the original aspect ratio.

When displaying the picture, just pass either the:original or:thumb option to the image url method:

views/photos/_photo.html.erb
1 <div>
2 <% if photo.image_data? %>
3   <%= image_tag photo.image_url(:thumb) %>
4 <% end %>
5 <p><%= photo.title %> | <%= link_to ‘Edit’, edit_photo_path(photo) %></p>
6 </div>

The following can be done within the form:
views/photos/_form.html.erb
1 <% if @photo.image_data? %>
2   <%= image_tag @photo.image_url(:thumb) %>
3   Remove attachment: <%= f.check_box :remove_image %>
4 <% end %>


You may use the delete raw: plugin to automatically remove the processed files when uploading is complete.

models/image_uploader.rb
1 plugin :delete_raw

Image’s Metadata
Aside from displaying a picture, you can also retrieve its information. Consider displaying the original photo’s size and MIME type:

views/photos/_photo.html.erb
1 <div>
2 <% if photo.image_data? %>
3   <%= image_tag photo.image_url(:thumb) %>
4   <p>
5     Size <%= photo.image[:original].size %> bytes<br>
6     MIME type <%= photo.image[:original].mime_type %><br>
7   </p>
8 <% end %>
9 <p><%= photo.title %> | <%= link_to ‘Edit’, edit_photo_path(photo) %></p>
10 </div>



What about its metrics? Unfortunately, they are not saved by default, although this is doable using the store dimensions plugin.

Image’s Dimensions

The store dimensions plugin is dependent on the fastimage gem, so instal it now:
1 gem ‘fastimage’

Remember to run:
1 bundle install

Now simply add the plugin:

models/image_uploader.rb

1 plugin :store_dimensions

Also, use the width and height methods to display the dimensions:
views/photos/_photo.html.erb
1 <div>
2 <% if photo.image_data? %>
3   <%= image_tag photo.image_url(:thumb) %>
4   <p>
5     Size <%= photo.image[:original].size %> bytes<br>
6     MIME type <%= photo.image[:original].mime_type %><br>
7     Dimensions <%= “#{photo.image[:original].width}x#{photo.image[:original].height}” %>
8   </p>
9 <% end %>
10 <p><%= photo.title %> | <%= link_to ‘Edit’, edit_photo_path(photo) %></p>
11 </div>


There is also a dimensions method that returns an array including width and height (for example, [500, 750]).

Moving to the Cloud

Cloud services are frequently used by developers to host uploaded files, and Shrine provides this option. This section will demonstrate how to upload files to Amazon S3.

Add two more gems to the Gemfile as a starting step:

1 gem “aws-sdk”, “~> 2.1”
2  group :development do
3   gem ‘dotenv-rails’
4 end


To interact with S3’s SDK, aws-sdk is required, while dotenv-rails will be utilised to manage environment variables during development.

1 bundle install

You must first get a key pair in order to access S3 through API. Sign in (or sign up) to Amazon Web Services Console and go to Security Credentials > Users to acquire it. Create a user with S3 file manipulation permissions. Here is a basic policy that allows unrestricted access to S3:

1 {
2 “Version”: “2016-11-14”,
3 “Statement”: [
4   {
5     “Effect”: “Allow”,
6     “Action”: “s3:*”,
7     “Resource”: “*”
8   }
9 ]
10 }


Download the key pair for the newly created user. You could also utilise root access keys, but I highly advise against doing so because it is extremely unsafe.

Next, establish an S3 bucket to host your files and add a configuration file to the project’s root:
.env
1 S3_KEY=YOUR_KEY
2 S3_SECRET=YOUR_SECRET
3 S3_BUCKET=YOUR_BUCKET
4 S3_REGION=YOUR_REGION

Never make this file available to the public, and keep it out of Git:

.gitignore
1 .env

Now, change the global settings of Shrine and add a new storage:

config/initializers/shrine.rb
1 require “shrine”
2 require “shrine/storage/s3”
3
4 s3_options = {
5   access_key_id:      ENV[‘S3_KEY’],
6   secret_access_key:  ENV[‘S3_SECRET’],
7   region:             ENV[‘S3_REGION’],
8   bucket:             ENV[‘S3_BUCKET’],
9 }
10
11 Shrine.storages = {
12   cache: Shrine::Storage::FileSystem.new(“public”, prefix: “uploads/cache”),
13   store: Shrine::Storage::S3.new(prefix: “store”, **s3_options),
14 }


There are no changes required to the rest of the software, and you may start using the increased storage right immediately. If you’re getting S3 problems about invalid keys, make sure you duplicated the key and secret correctly, without any trailing spaces or invisible special symbols.

Conclusion

Hopefully, you now have a good understanding of Shrine and are excited to use it in one of your projects. Many of the characteristics of this gem have already been described, but there are a few more, such as the ability to keep extra context with files and the direct upload method. So, have a look at Shrine’s documentation and official website, which clearly has details of all accessible plugins. 

0
Afreen Khalfe

Afreen Khalfe

A professional writer and graphic design expert. She loves writing about technology trends, web development, coding, and much more. A strong lady who loves to sit around nature and hear nature’s sound.

Add comment