Monday, December 26, 2016

Uploading Files With Rails and Shrine

Uploading Files With Rails and Shrine

There are many file uploading gems out there like CarrierWave, Paperclip, and Dragonfly, to name a few. They all have their specifics, and probably you've already used at least one of these gems.

Today, however, I want to introduce a relatively new, but very cool solution called Shrine, created by Janko Marohnić. In contrast to some other similar gems, it has a modular approach, meaning that every feature is packed as a module (or plugin in Shrine's terminology). Want to support validations? Add a plugin. Wish to do some file processing? Add a plugin! I really love this approach as it allows you to easily control which features will be available for which model.

In this article I am going to show you how to:

  • integrate Shrine into a Rails application
  • configure it (globally and per-model)
  • add the ability to upload files
  • process files
  • add validation rules
  • store additional metadata and employ file cloud storage with Amazon S3

The source code for this article is available on GitHub.

The working demo can be found here.

Integrating Shrine

To start off, create a new Rails application without the default testing suite:

I will be using Rails 5 for this demo, but most of the concepts apply to versions 3 and 4 as well.

Drop the Shrine gem into your Gemfile:

Then run:

Now we will require a model that I am going to call Photo. Shrine stores all file-related information in a special text column ending with a _data suffix. Create and apply the corresponding migration:

Note that for older versions of Rails, the latter command should be:

Configuration options for Shrine can be set both globally and per-model. Global settings are done, of course, inside the initializer file. There I am going to hook up the necessary files and plugins. Plugins are used in Shrine to extract pieces of functionality into separate modules, giving you full control of all the available features. For example, there are plugins for validation, image processing, caching attachments, and more.

For now, let's add two plugins: one to support ActiveRecord and another one to set up logging. They are going to be included globally. Also, set up file system storage:

config/initializers/shrine.rb

Logger will simply output some debugging information inside the console for you saying how much time was spent to process a file. This can come in handy.

All uploaded files will be stored inside the public/uploads directory. I don't want to track these files in Git, so exclude this folder:

.gitignore

Now create a special "uploader" class that is going to host model-specific settings. For now, this class is going to be empty:

models/image_uploader.rb

Lastly, include this class inside the Photo model:

models/photo.rb

[:image] adds a virtual attribute that will be used when constructing a form. The above line can be rewritten as:

Nice! Now the model is equipped with Shrine's functionality, and we can proceed to the next step.

Controller, Views, and Routes

For the purposes of this demo, we'll need only one controller to manage photos. The index page will serve as the root:

pages_controller.rb

The view:

views/photos/index.html.erb

In order to render the @photos array, a partial is required:

views/photos/_photo.html.erb

image_data? is a method presented by Shrine that checks whether a record has an image.

image_url is yet another Shrine method that simply returns a path to the original image. Of course, it is much better to display a small thumbnail instead, but we will take care of that later.

Add all the necessary routes:

config/routes.rb

This is it—the groundwork is done, and we can proceed to the interesting part!

Uploading Files

In this section I will show you how to add the functionality to actually upload files. The controller actions are very simple:

photos_controller.rb

The only gotcha is that for strong parameters you have to permit the image virtual attribute, not the image_data.

photos_controller.rb

Create the new view:

views/photos/new.html.erb

The form's partial is also trivial:

views/photos/_form.html.erb

Once again, note that we are using the image attribute, not the image_data.

Lastly, add another partial to display errors:

views/shared/_errors.html.erb

This is pretty much all—you can start uploading images right now.

Validations

Of course, much more work has to be done in order to complete the demo app. The main problem is that the users may upload absolutely any type of file with any size, which is not particularly great. Therefore, add another plugin to support validations:

config/inititalizers/shrine.rb

Set up the validation logic for the ImageUploader:

models/image_uploader.rb

I am permitting only JPG and PNG images less than 1MB to be uploaded. Tweak these rules as you see fit.

MIME Types

Another important thing to note is that, by default, Shrine will determine a file's MIME type using the Content-Type HTTP header. This header is passed by the browser and set only based on the file's extension, which is not always desirable.

If you wish to determine the MIME type based on the file's contents, then use a plugin called determine_mime_type. I will include it inside the uploader class, as other models may not require this functionality:

models/image_uploader.rb

This plugin is going to use Linux's file utility by default.

Caching Attached Images

Currently, when a user sends a form with incorrect data, the form will be displayed again with errors rendered above. The problem, however, is that the attached image will be lost, and the user will need to select it once again. This is very easy to fix using yet another plugin called cached_attachment_data:

models/image_uploader.rb

Now simply add a hidden field into your form.

views/photos/_form.html.erb

Editing a Photo

Now images can be uploaded, but there is no way to edit them, so let's fix it right away. The corresponding controller's actions are somewhat trivial:

photos_controller.rb

The same _form partial will be utilized:

views/photos/edit.html.erb

Nice, but not enough: users still can't remove an uploaded image. In order to allow this, we'll need—guess what—another plugin

models/image_uploader.rb

It uses a virtual attribute called :remove_image, so permit it inside the controller:

photos_controller.rb

Now just display a checkbox to remove an image if a record has an attachment in place:

views/photos/_form.html.erb

Generating a Thumbnail Image

Currently we display original images, which is not the best approach for previews: photos may be large and occupy too much space. Of course, you could simply employ the CSS width and height attributes, but that's a bad idea as well. You see, even if the image is set to be small using styles, the user will still need to download the original file, which might be pretty big.

Therefore, it is much better to generate a small preview image on the server side during the initial upload. This involves two plugins and two additional gems. Firstly, drop in the gems:

Image_processing is a special gem created by the author of Shrine. It presents some high-level helper methods to manipulate images. This gem, in turn, relies on mini_magick, a Ruby wrapper for ImageMagick. As you've guessed, you'll need ImageMagick on your system in order to run this demo.

Install these new gems:

Now include the plugins along with their dependencies:

models/image_uploader.rb

Processing is the plugin to actually manipulate an image (for example, shrink it, rotate, convert to another format, etc.). Versions, in turn, allows us to have an image in different variants. For this demo, two versions will be stored: "original" and "thumb" (resized to 300x300).

Here is the code to process an image and store its two versions:

models/image_uploader.rb

resize_to_limit! is a method provided by the image_processing gem. It simply shrinks an image down to 300x300 if it is larger and does nothing if it's smaller. Moreover, it keeps the original aspect ratio.

Now when displaying the image, you just need to provide either the :original or :thumb argument to the image_url method:

views/photos/_photo.html.erb

The same can be done inside the form:

views/photos/_form.html.erb

To automatically delete the processed files after uploading is complete, you may add a plugin called delete_raw:

models/image_uploader.rb

Image's Metadata

Apart from actually rendering an image, you may also fetch its metadata. Let's, for example, display the original photo's size and MIME type:

views/photos/_photo.html.erb

What about its dimensions? Unfortunately, they are not stored by default, but this is possible with a plugin called store_dimensions.

Image's Dimensions

The store_dimensions plugin relies on the fastimage gem, so hook it up now:

Don't forget to run:

Now just include the plugin:

models/image_uploader.rb

And display the dimensions using the width and height methods:

views/photos/_photo.html.erb

Also, there is a dimensions method available that returns an array containing width and height (for example, [500, 750]).

Moving to the Cloud

Developers often choose cloud services to host uploaded files, and Shrine does present such a possibility. In this section, I will show you how to upload files to Amazon S3.

As the first step, include two more gems into the Gemfile:

aws-sdk is required to work with S3's SDK, whereas dotenv-rails will be used to manage environment variables in development.

Before proceeding, you should obtain a key pair to access S3 via API. To get it, sign in (or sign up) to Amazon Web Services Console and navigate to Security Credentials > Users. Create a user with permissions to manipulate files on S3. Here is the simple policy presenting full access to S3:

Download the created user's key pair. Alternatively, you might use root access keys, but I strongly discourage you from doing that as it's very insecure.

Next, create an S3 bucket to host your files and add a file into the project's root to host your configuration:

.env

Never ever expose this file to the public, and make sure you exclude it from Git:

.gitignore

Now modify Shrine's global configuration and introduce a new storage:

config/initializers/shrine.rb

That's it! No changes have to be made to the other parts of the app, and you can test this new storage right away. If you are receiving errors from S3 related to incorrect keys, make sure you accurately copied the key and secret, without any trailing spaces and invisible special symbols.

Conclusion

We've come to the end of this article. Hopefully, by now you feel much confident in using Shrine and are eager to employ it in one of your projects. We have discussed many of this gem's features, but there are even more, like the ability to store additional context along with files and the direct upload mechanism. 

Therefore, do browse Shrine's documentation and its official website, which thoroughly describes all available plugins. If you have other questions left about this gem, don't hesitate to post them. I thank you for staying with me, and I'll see you soon! 


No comments:

Post a Comment