logo <

Introducing Shrine – A file upload toolkit

I’m really excited about this. I’ve just released Shrine, a new solution for handling file uploads in Ruby applications. It was heavily inspired by Refile, most notably its idea of backends. However, unlike Refile, it is designed primarily for upfront processing (as opposed to on-the-fly). It’s also inspired by CarrierWave’s idea of uploaders.

Flexibility

Shrine implements a plugin system analogous to Roda’s and Sequel’s. It has a small core which provides only the essential functionality, while other features come as plugins which can be loaded when needed. Shrine ships with over 25 plugins, which together provide a great arsenal of features.

This design makes Shrine extremely versatile. File uploads are very delicate, and need to be handled differently depending on what types of files are being uploaded, whether there is processing or not, what storage is used etc. Instead of having an opinion on how you want to do your upload, Shrine allows you to build an uploading flow that suits your needs.

Shrine.plugin :sequel
Shrine.plugin :pretty_location
Shrine.plugin :logging, format: :json
class ImageUploader < Shrine
  # ...
end
class User < Sequel::Model
  include ImageUploader::Attachment(:avatar)
end
user = User.create(avatar: File.open("path/to/avatar.jpg"))
user.avatar.id # "user/532/avatar/f753g598sm3l2.jpg"

Simplicity

Where CarrierWave and other file upload libraries favor complex class-level DSLs, Shrine favours simple instance-level interface. Here’s an example on how file processing is done in Shrine:

# Gemfile
gem "image_processing", "~> 1.2"
require "image_processing/mini_magick"

class ImageUploader < Shrine
  plugin :processing
  plugin :versions

  process(:store) do |io, context|
    next if context[:record].guest? # we have access to the record

    versions = { original: io }

    io.download do |original|
      processor = ImageProcessing::MiniMagick.source(original)

      versions[:large]  = processor.resize_to_limit!(800, 800)
      versions[:medium] = processor.resize_to_limit!(500, 500)
      versions[:small]  = processor.resize_to_limit!(300, 300)
    end

    versions
  end
end

This method gets called whenever a file is uploaded, so you can just use regular Ruby to specify exactly how and when processing is done. You can also choose to do some processing on caching as well.

Validations are done in a similar fashion:

class ImageUploader < Shrine
  plugin :validation_helpers

  Attacher.validate do
    # Evaluated inside an instance of Shrine::Attacher.
    unless record.admin?
      validate_max_size 2*1024*1024, message: "is too large (max is 2 MB)"
      validate_mime_type_inclusion ["image/jpg", "image/png", "image/gif"]
    end
  end
end

Another difference from other gems is number of obligatory dependencies. While CarrierWave, Refile and Paperlip have 9-12 depedencies in total, Shrine by default has only 2 – one for downloading files, one for generating a value for the Content-Disposition header.

Performance

Shrine cares a lot about performance. For example, it allows you to minimize file copying by moving files instead, which is useful when dealing with larger files, and also means that no temporary files will be left behind.

Shrine also comes with a parallelize plugin, which uploads and deletes files in parallel. This is used when you have multiple versions of your files.

Background jobs

Now we come to a major difference between Shrine and other uploading gems. Other gems aren’t designed to support backgrounding, and although external gems exist that add this functionality (e.g. carrierwave_backgrounder), they require complex setup and in my experience have been very unstable (e.g. carrierwave_backgrounder breaks removing attachments).

Shrine, on the other hand, embraces that putting phases of file upload into background jobs is essential for good user experience and scaling, and is designed from the very beginning with this in mind. It comes with a backgrounding plugin, which allows you to put processing, storing and deleting into a background job:

Shrine.plugin :backgrounding
Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
class UploadJob
  include Sidekiq::Worker
  def perform(data)
    Shrine::Attacher.promote(data)
  end
end
class DeleteJob
  include Sidekiq::Worker
  def perform(data)
    Shrine::Attacher.delete(data)
  end
end

Notice that, unlike gems like carrierwave_backgrounder, you are required to write your own job classes, but as you can see, Shrine makes the implementation very simple. In this example I used Sidekiq, but obviously you can just as well use any other backgrounding library.

The end user experience was the main guidance in Shrine’s design. Before the file is moved to store, the record is first saved with the cached version of the file. This means that, while the file is being processed and stored in the background, the end user will immediately see the image they uploaded, because the URL will point to the cached version. So from the user’s perspective, at this moment the file upload is finished!

user.avatar.url #=> "/uploads/dso3432kdw032.jpg"
# ... Background job is done storing ...
user.avatar.url #=> "https://s3-sa-east-1.amazonaws.com/my-bucket/0943sf8gfk13.jpg"

When the background job finishes, the record will be updated with the stored version, but the user won’t notice that the URL has changed, because they will still see the same image. And this is the goal, to make the end user completely unaware of the internal complexity.

Direct uploads

Like Refile, Shrine also supports direct uploads. This means you can cache files using AJAX, before the form is submitted. This generally provides the best user experience, because the UI isn’t blocked, and the user knows how much they have to wait (assuming you give them a progress bar). The endpoint for direct uploads is provided by the direct_upload plugin.

class ImageUploader < Shrine
  plugin :direct_upload
end
Rails.application.routes.draw do
  # adds `POST /attachments/images/:storage/:name`
  mount ImageUploader.direct_endpoint => "/attachments/images"
end

Unlike Refile, Shrine doesn’t ship with complete JavaScript which makes this just work, instead it expects you to use an existing JavaScript library for file uploads (jQuery-File-Upload is really good). This plugin also provides a presign route which you can use for implementing direct S3 uploads. I created an example app to demonstrate how easy it is to implement multiple uploads directly to S3.

Safety

File uploads can bring many security vulnerabilities, and Shrine tries to be as secure as possible.

For example, for extracting image dimensions it uses the fastimage gem, which has built-in protection against image bombs. Shrine also ships with the determine_mime_type plugin which enables you to extract the actual MIME type of a file (by default it uses the UNIX file utility).

Shrine normally does processing before storing, which happens after validation (I say “normally” because you can also choose to process on caching, depending on your situation). I mention this because CarrierWave does processing before validation, which is a huge security flaw since it allows attackers to easily DoS your application by uploading large images (#1320).

Shrine also implements backgrounding in a very safe way. For example, it could potentially happen that the user changes the attachment before the background job is finished processing and storing. In this situation a naive implementation would replace a new file with an old stored one, but Shrine, once it’s done with processing and storing, checks if the attachment has changed, and if it did it doesn’t do the replacement.

Conclusion

Shrine ships with a lot of other plugins that I haven’t managed to cover here, but I encourage you to check them out. I spent a lot of time studying other solutions and their open issues, and hopefully I succeeded in making Shrine the next level of file uploads.

Janko Marohnić

comments powered by Disqus