Back

Moving from Active Storage to Shrine

Captain's log, stardate d576.y40/AB

Shrine Active Storage
Oriol Collell Martín
Backend developer
Moving from Active Storage to Shrine

At MarsBased we love Ruby, and we love Rails. Ruby on Rails is one of the primary components we use to build web applications. However, this does not necessarily mean we love everything contained within Rails. We have a strong opinion on some of its components. One of that is Active Storage.

About Active Storage

Active Storage is a great starting point for many web applications, and it has a lot of benefits. It's a drop-in component with no external dependencies that allows a web application to have file uploads. It manages all the low-level details and is designed to be flexible enough for most scenarios. However, following the convention-over-configuration principle that Rails follows, it's an opinionated piece of software with which one may or may not agree.

The way Active Storage works is by adding a new table to the database to store references to all uploaded objects across all application models. It includes a polymorphic belongs_to association, so it can be associated with any other model:

The key benefit of this design is that it does not require any database modifications when we want to add a file upload to a new model.

However, this is precisely one of the aspects of Active Storage that we don't like at MarsBased. Using separate tables requires having to JOIN both tables in a database query in order to load the data. In a web application, most of the time you need to retrieve a record from the database, you will need its associated uploads too because it's an intrinsic part of it. In our opinion, the profile picture of a user, for instance, is as important as its name.

Having everything in a single table makes queries simpler and more performant.

The other key aspect of Active Storage that we don't like is how variants are managed. In Active Storage, variants are transformations to the original file: it can be an image resize, a format conversion, etc. In Active Storage, variants are generated on the fly the first time they are requested. The benefit of this approach is that used storage space is reduced because all variant files that are never accessed are never generated. The downside is that the first request takes longer to respond and may crash if there is a problem with the file manipulation.

We prefer to have a more predictable behavior and generate all file variations after a file is uploaded. Also, we use Sidekiq to generate variants so we can have more complex and time-consuming transformations and automatic retries if they fail. With this, we ensure that any variant will be available already when the first request for it arrives.

Moving onto Shrine

We have used different libraries to manage file uploads during the MarsBased life: CarrierWave and Paperclip, for instance. However, over the last few years, we have stuck to using Shrine.

Shrine works very similarly to CarrierWave and other similar libraries. The key points are:

For each file, you need to define an Uploader describing the particularities of that file and then include it in the model. This is how a user having a profile picture could be implemented.

    class User < ApplicationRecord
        include UserPhotoUploader::Attachment(:photo)
    end

    class UserPhotoUploader < Shrine

        ACCEPTED_FORMATS = %w[jpeg jpg png gif heic].freeze

        plugin :determine_mime_type, analyzer: :marcel,
                                            analyzer_options: { filename_fallback: true }
        plugin :validation_helpers
        plugin :derivatives, create_on_promote: true

        Attacher.validate do
            validate_max_size 10.megabytes
            validate_extension_inclusion ACCEPTED_FORMATS
        end

        Attacher.derivatives do |original|
            vips = ImageProcessing::Vips.source(original)

            {
                thumbnail: vips.resize_to_limit!(500, 500)
            }
        end

    end

Then, you need an initializer to define the common configuration across all uploaders, including the remote storage to use. This is a simplification of the configuration we use in most projects.

    # frozen_string_literal: true

    require 'shrine'
    require 'shrine/storage/s3'

    Shrine.plugin :activerecord
    Shrine.plugin :determine_mime_type
    Shrine.plugin :cached_attachment_data
    Shrine.plugin :restore_cached_data
    Shrine.plugin :backgrounding
    Shrine.plugin :instrumentation
    Shrine.plugin :pretty_location

    Shrine::Attacher.promote_block do
        AttachmentPromoteWorker.perform_async(
            self.class.name, record.class.name, record.id, name.to_s, file_data
        )
    end
    Shrine::Attacher.destroy_block do
        AttachmentDestroyWorker.perform_async(self.class.name, data)
    end

    default_config = {
        bucket: "app-#{ENV.fetch('SITE_ENV', nil)}",
        access_key_id: Rails.application.credentials.aws&.access_key_id,
        secret_access_key: Rails.application.credentials.aws&.secret_access_key,
        region: 'eu-west-1'
    }

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

These are the key points of our configuration that have worked very well for us so far:

Conclusion

We have been using Shrine for several years and it has proven to be a highly effective tool for us. It is powerful, the code is clean, and its modular design allows us to include only the necessary components.

However, it's important to note that there is no one-size-fits-all solution. At MarsBased, we always evaluate alternative options to determine what best suits our needs for each project. While Shrine is our preferred choice for most projects, we also recognize that Active Storage may be a better fit for certain projects depending on their specific requirements.

Share this post

Related Articles

Space station from the inside

Client side file uploads with Amazon S3

S3 client-side uploads on top of Middleman using a simple Rails application to authorize file uploads and downloads.

Read full article
An alternative to Active Storage: Shrine

An alternative to Active Storage: Shrine

We are using Shrine as an alternative to Active Storage for our own product. Here's why.

Read full article
S3 versioning and lifecycles

S3 versioning and lifecycles

As a development agency, we're no strangers to also building the technical architecture for our clients. That includes using Amazon's S3 service for storage, which we've used in many projects to make our clients' life easier.

Read full article