Working with Rails 7 and ActiveStorage
Every developer has one of those platform technologies that never seems to work well for them – and for me, for Rails, it has always been ActiveStorage, specifically with respect to images. I’ve been trying to use ActiveStorage this way since its initial release and I’ve just had crappy, crappy luck with it.
I’ve now started a new side project, imgarr, which depends entirely on ActiveStorage so I guess this is the year where I get serious and really learn ActiveStorage. This is what I refer to as the “going all in approach to learning something”.
Note: imgarr is currently in development and, like a platypus doesn’t so much.
What is ActiveStorage
ActiveStorage is a core Rails library which:
- handles cloud storage of blob objects
- powers basecamp
- includes image processing as quasi native operations
Code Examples
Let’s say that you have an image model in ActiveRecord which looks like this:
# == Schema Information
#
# Table name: images
#
# id :bigint not null, primary key
# date_created_at :date not null
# dollar_cost :decimal(8, 2)
# filename :string not null
# filesize :bigint
# image_type :string
# ip_address :string
# name :string
# short_name :string
# user_agent :string
# view_count :bigint
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_images_on_date_created_at_and_id (date_created_at,id)
# index_images_on_short_name (short_name) UNIQUE
# index_images_on_user_id (user_id)
# index_images_on_user_id_and_id (user_id,id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
class Image < ApplicationRecord
belongs_to :user
validates :user_id, presence: true
validates :date_created_at, presence: true
validates :short_name, uniqueness: true
scope :ordered, -> { order("id DESC") }
has_one_attached :main_image do |attachable|
attachable.variant :thumbnail, resize: "100x100"
attachable.variant :medium, resize: "300x300"
end
end
Let’s ignore the block with attachable and start with the commands to create a square variant of 250x250 pixels:
image.main_image.variant(resize_to_fit: [250, 250])
That’s going to create a square image of 250 x 250 pixels. Now the first thing you are likely to notice is that image – if it was a phone picture taken in vertical orientation is that the aspect ratio is going to be off. The reason is that a transformation of 1:1 was applied to an image with a non 1:1 aspect ratio. The solution is to specify nil for one of the orientations. Let’s try this again with:
image.main_image.variant(resize_to_fit: [250, nil])
Now you’re going to get an image that is 250 pixels wide and a corresponding height based on the image’s underlying aspect ratio. That’s tremendously cool. Here’s the opposite orientation specified as nil:
image.main_image.variant(resize_to_fit: [nil, 250])
Let’s pull out Rails console and do some introspection on the methods available to the above code:
(image.main_image.variant(resize_to_fit: [250, nil]).methods - Object.methods).sort
[
[0] :blob,
[1] :download,
[2] :image,
[3] :key,
[4] :process,
[5] :processed,
[6] :processed?,
[7] :service,
[8] :url,
[9] :variation
]
Each of these methods is illustrated below.
blob
This gives you access to the raw blob of data represented by the ActiveStorage object:
image.main_image.variant(resize_to_fit: [250, nil]).blob
#<ActiveStorage::Blob:0x000000010e15d060> {
:id => 26,
:key => "mp6439g1huwfonfmkodbz9q5b2as",
:filename => #<ActiveStorage::Filename:0x000000010e014ff0 @filename="IMG_3762.jpg">,
:content_type => "image/jpeg",
:metadata => {
"identified" => true
},
:service_name => "local",
:byte_size => 4227649,
:checksum => "MjQVnFP8ANWqc3XqiZnAfA==",
:created_at => Mon, 13 Feb 2023 09:37:16.525802000 UTC +00:00
}
download
image
key
process
processed
processed?
service
url
variation
image.main_image.variant(resize_to_fit: [250, nil]) ActiveStorageAttachment Load (0.5ms) SELECT “active_storage_attachments”.* FROM “active_storage_attachments” WHERE “active_storage_attachments”.”record_id” = $1 AND “active_storage_attachments”.”record_type” = $2 AND “active_storage_attachments”.”name” = $3 LIMIT $4 [[“record_id”, 24], [“record_type”, “Image”], [“name”, “main_image”], [“LIMIT”, 1]] ActiveStorageBlob Load (0.6ms) SELECT “active_storage_blobs”.* FROM “active_storage_blobs” WHERE “active_storage_blobs”.”id” = $1 LIMIT $2 [[“id”, 26], [“LIMIT”, 1]] #<ActiveStorageVariantWithRecord:0x000000010e1c57c8 @blob=#<ActiveStorageBlob id: 26, key: “mp6439g1huwfonfmkodbz9q5b2as”, filename: “IMG_3762.jpg”, content_type: “image/jpeg”, metadata: {“identified”=>true}, service_name: “local”, byte_size: 4227649, checksum: “MjQVnFP8ANWqc3XqiZnAfA”, created_at: “2023-02-13 09:37:16.525802000 +0000”>, @variation=#<ActiveStorageVariation:0x000000010e1c5930 @transformations={:format=>”jpg”, :resize_to_fit=>[250, nil]}» irb(main):003:0> (image.main_image.variant(resize_to_fit: [250, nil]).methods - Object.methods).sort [ [0] :blob, [1] :download, [2] :image, [3] :key, [4] :process, [5] :processed, [6] :processed?, [7] :service, [8] :url, [9] :variation ]