<Ruby on Rails> Implementing an Image Hosting Service Using Active Storage
The following is a tech blog I wrote while working at CyberOwl, Inc. The original version, written in Japanese, can be found here.
CyberOwl Inc. provides services that heavily utilise images, and I developed an image hosting service for them. It has utilised Ruby as the server-side language and Ruby on Rails as the framework for development. This article aims to explain the design method of the image hosting service.
What is Image Hosting?
Image hosting involves using hosting services to improve webpage loading speeds when images are used. These services work by returning stored images when accessed via specific URLs, allowing web pages to embed images using the img tag. This service also offers the advantage of easily changing image size, resolution, and format. In our project, we've enabled specifying image sizes and resolutions using query parameters.
Uploading images by utilising Active Storage Direct Upload in Ruby on Rails
What is Active Storage?
Active Storage in Ruby on Rails facilitates uploading files to cloud storage services and linking them to Active Record models in Rails. It also supports direct uploads from the client to cloud storage services using an accompanying JavaScript library.
Directly upload images to Amazon S3
When using Active Storage for direct uploads to Amazon S3, Rails processes as follows:
Note: The sequence diagram for image uploads is not numerically aligned with the listed steps.
Problem with Active Storage DirectUploader and its Solution
Problem: Unable to organise folders in S3
When using Ruby on Rails' ActiveStorage DirectUploadsController as it is, images are saved in S3 without being organised into folders. This becomes a problem when multiple services use the image hosting service, as all images end up mixed together in the S3 bucket. In situations where a service is discontinued, identifying and bulk deleting the images used by that service from the S3 bucket becomes challenging if the images are not organised into folders.
Solve the problem by inheriting from DirectUploadsController
When using direct upload to upload images to an S3 bucket, the Rails server first creates a record in the active_storage_blobs table, assigning the blob record's key a randomly generated unique string like "1ym4o2y9mauvjgincwfcsolx4hul". This key becomes the object name in S3 during image storage. If the blob record's key (S3 object name) is "blog/1ym4o2y9mauvjgincwfcsolx4hul", for example, the image object will be stored in the blog folder within S3. By setting the blob record's key to "service name / unique string", folder organisation based on the service can be achieved. To implement this, a new controller was created by inheriting from DirectUploadsController, and the create function responsible for generating blob records was overridden.
This is the create function inside ActiveStorage::DirectUploadsController.
def create{
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
render json: direct_upload_json(blob)
}
The code shown below corresponds to the organisation of folders in the S3 bucket. In this system, images are stored in two layers of folders within the S3 bucket, with organisation based on media type and categories.
The blob record is created by the create_before_direct_upload method of ActiveStorage::Blob, and updating the blob.key allows for changing the key used for uploads to S3.
def create{
#↓Only the part directly related to the content of this blog
prefix = File.join(media, category)
blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
blob.key = File.join(prefix, blob.id)
blob.save
render json: direct_upload_json(blob)
}
Editing images with MiniMagick
When you access the URL for image retrieval with parameters like blob_id, resolution, size, and image format, it returns an image edited to the specified specifications. To achieve this, it's necessary to edit image size and sometimes change file formats. In Ruby, this can be done using a gem called MiniMagick, which allows for programmatic interaction with ImageMagick, a command-line tool for image editing. MiniMagick was utilised for editing images in this context.
At this stage, the image is downloaded from S3, and its size and resolution are edited to create a Variant (edited image). Then, a variant record is added to the active_storage_blobs table and uploaded back to S3.
Recommended by LinkedIn
Edit the image based on the variables in the URL query and create a Variant record
The image retrieval URL is formatted as follows:
“/image_hosting/blob_ID.jpg?quality=80&resize=2048”
Here's a detailed explanation:
The code for editing and sending images using query parameters is as follows.
image_info = params[:id];
id = image_info.split('.')[0]
image = ActiveStorage::Blob.find(id)
image_format = image_info.split('.')[1] || original_format
quality = params.fetch(:quality, 80).to_i
resize = params[:resize] || 2048
resize = [2048, resize.to_i].min
variant = image.variant(
resize: resize,
gravity: "center",
quality: quality,
format: image_format
).processed
variant_image = variant.image.service.download(variant.key)
send_data variant_image, type: variant.image.blob.content_type, disposition: 'inline'
Problems and Solutions When Adding Variant Records
The creation of Variant records in Ruby on Rails using Active Storage encounters the same issue as image saving, where edited images are not organised into folders in S3. This defeats the purpose of folder organisation implemented during image storage. To resolve this, the S3 key used during variant creation needs to be edited. This is achieved by modifying the transform_blob function, which is called by the processed function responsible for creating the variant.
Original transform_blob function (models/active_storage/variant_record.rb)
def transform_blob
blob.open do |input|
variation.transform(input) do |output|
yield io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
content_type: variation.content_type, service_name: blob.service.name
end
end
end
Edited transform_blob function (models/active_storage/variant_record.rb)
def transform_blob
blob.open do |input|
variation.transform(input) do |output|
prefix = blob.key.split('/')[0...-1].join("/") + "/variant" || "Unknown_Variant"
v_key = File.join(prefix, extract_blob_id(blob), ActiveStorage::Blob.generate_unique_secure_token)
yield key: v_key, io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
content_type: variation.content_type, service_name: blob.service.name
end
end
end
In the yield block, the create_or_find_record function is called with the blob record as its argument. Therefore, edits are made to this key. A variant folder is created within the folder where the original image is stored, and the variant images are saved in this variant folder.
Image Retrieval
When retrieving images using a specified URL, the server can return the image in one of two ways:
For a new image request, the server edits the original image using MiniMagick and returns it. However, for requests with the same parameters that have been made before, the server returns the image previously edited and stored in S3. Rails makes this possible by creating a variable called variant_digest from the image retrieval URL parameters, which is used to check if a specified image has been created before by comparing it with variant_digest records stored in the database.
The flow for retrieving edited images using the image retrieval URL is depicted in the following diagram.
Other Features
In addition to image uploading and hosting, the following features have also been implemented. When it comes to actually deploying the image hosting service to a production environment, the following features should be implemented.