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.
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.
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:
:backgrounding
plugin. When a request uploads a file, it is first uploaded to the cache
and a background job is enqueued to process it. The background job retrieves the file from the cache, generates all the derivatives, and uploads them to the definitive storage. Processing in the background reduces the load on the requesting process and improves the overall resilience of the system.cache
in the local filesystem, we store it in AWS S3. This allows us to display the cached image even when a form with validation errors is uploaded, as well as on any page before the background job finishes processing it.:pretty_location
plugin to organize the S3 bucket in a more structured manner. This simplifies manual search and clean-up operations.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.
S3 client-side uploads on top of Middleman using a simple Rails application to authorize file uploads and downloads.
Read full articleWe are using Shrine as an alternative to Active Storage for our own product. Here's why.
Read full articleAs 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