
Ruby on Rails, our beloved framework, offers a new standard for uploading files. Welcome to ActiveStorage!
This article is a very quick and straightforward guide to getting started with ActiveStorage.
I've hosted a working example application on Github so you can try it out as it is. It illustrates most of the items presented in this article. The link is at the end of the article.
Table of contents:
There are no gems to add to your Gemfile because Rails 5.2 comes with ActiveStorage built in. Just execute rails active_storage:install which will generate a migration file and then execute Rake db:migrate.
If you are reading this migration (always be curious!) , you can see that it adds two tables to your database:
Active_Storage_Blobs : this table records blobs, which are information about files (file name, metadata, size, etc.).active_storage_attachments : it is a junction table between the models and the blobs in your application.So far, you've probably been used to:
ActiveStorage removes these two steps. You no longer need to generate a migration for your models to have one or more attachments.
With ActiveStorage, all attachments of all models will be recorded in Active_Storage_Blobs and active_storage_attachments (a polymorphic association) will be the link between your models and your blobs. If all of this is still confusing for you, don't worry, we'll get to that point shortly, it's actually pretty easy to understand.
For now, let's focus on configuration. We've generated a migration and migrated the database, now we need to tell ActiveStorage where to store the uploaded files.
Read first config/storage.yml. This file allows you to define multiple storage strategies. Each environment will be assigned a storage strategy.
Here is the file config/storage.yml: generated by default:
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket
# Remember not to checkin your GCS keyfile to a repository
# google:
# service: GCS
# project: your_project
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
# storage_account_name: your_account_name
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name
# mirror:
# service: Mirror
# primary: local
# mirrors: [ amazon, google, microsoft ]Each storage strategy essentially tells ActiveStorage two things:
Which service to use (choose between Disk,S3,GCS,Azure Storage and Mirror)
The list of services is quite simple to understand:
Disk : Store files on your local driveS3 : Use Amazon S3 (requirement: add gem 'aws-sdk-s3' to your Gemfile)GCS : Use Google Cloud Storage (requirement: add gem 'google-cloud-storage', '~> 1.11' to your Gemfile)Azure Storage : Use Microsoft Azure Storage (requirement: add Gem 'azure-storage' to your Gemfile)Then there is Mirror. Mirror which tells ActiveStorage to use both a primary storage strategy and a collection of other strategies to make copies of your uploaded documents. Did you want an easy way to create backups for uploaded documents? Mirror is a good solution.
One last thing about the mirror service: while copies are made, all of your requests and downloads will be made to/from the primary strategy. This is a backup mechanism, not a load balancing mechanism. So let's go back to config/storage.yml and to your list of storage strategies.
As in the example above, you could choose to have:
testing for when you run your rspec/minitest/whatever. In this strategy, you'll probably want to store the uploaded files in rails.root.join (“tmp/storage”)in order to be able to clean them by running Rake tmp:clean.locale for the development environment. This would allow uploaded files to be stored in a non-volatile storage space, say in rails.root.join (“storage”) for example.Amazon for a production environment. This would allow uploaded files to be stored in an Amazon S3 bucket.I am not going to explain the specifics of the configuration of each service because they are quite explicit. Just read the examples above and you're pretty much done. Oh and, of course, don't forget to configure your external services on their respective platforms beforehand (e.g. for S3, create a bucket and set the right permissions).
Once you've written your storage policies (you can keep the default policies for now), you need to assign a policy to each environment you run.
Concretely: in each config/environments/*.rb , define the attribute config.active_storage.service to the strategy you want.
For example, I usually have in config/environments/development.rb the following line: config.active_storage.service =:local.
Has_One_Attached)Model side:
avatar To one Profile in profile. has_one_attached: avatarReminder : you don't need to add a new column to your database table!
Now you can use some_profile.avatar.attached? to check if a file is present or not.
Controller side:
To allow an avatar to be uploaded, add :avatar with your permission
params.require(:profile).permit(:some_attribute, :some_other_attribute, :avatar)View side:
<%= form.file_field :avatar %>That's it!
has_many_attached)Model side:
Contracts pdf of contracts to a template of Customer. has_many_attached:contractsController side:
To allow new contracts to be uploaded, add contracts: [] to your authorized settings:
params.require(:customer).permit(:some_attribute, :yet_another_attribute, contracts: [])You can now use the option some_customer.contracts.attached? to check if at least one file is present or not.
View side:
<%= form.file_field :contracts, multiple: true %>some_profile.avatar.attached?Because the location of the file depends on the storage strategy, ActiveStorage provides help that creates a temporary redirection link to the file.
Create a redirection link that will last 5 minutes:
url_for(some_profile.avatar)Create a download link usingrails_blob_urlgoldrails_blob_path:
rails_blob_path(some_profile.avatar, disposition: 'attachment')binary_data = some_profile.avatar.downloadBe careful when doing this on large files stored on the cloud!
You can also delete an attachment:
some_profile.avatar.purgesome_profile.avatar.purge_later. This will program an ActiveJob to take care of it.You may also want to allow a user to delete attachments. I can offer you two solutions:
accept_nested_attributes_for. Let me explain this one to you.I assume you are used to using accept_nested_attributes_for.
When you add has_many_attached:contracts to a model of Customer, ActiveStorage also injects has_many_attached:contracts in your model.
Read this to find out exactly what's going on behind the scene: https://github.com/rails/rails/blob/0f57f75008242d1739326fec38791c01852c9aa7/activestorage/lib/active_storage/attached/model.rb
Here's how you'll allow contract attachments to be destroyed:
# Model
class Profile < ApplicationRecord
has_one_attached :avatar
accepts_nested_attributes_for :avatar_attachment, allow_destroy: true
end
# Controller
class ProfilesController < ApplicationController
before_action :set_profile, only: [:show, :edit, :update, :destroy]
# [...]
# PATCH/PUT /profiles/1
# PATCH/PUT /profiles/1.json
def update
respond_to do |format|
if @profile.update(profile_params)
format.html { redirect_to @profile, notice: 'Profile was successfully updated.' }
format.json { render :show, status: :ok, location: @profile }
else
format.html { render :edit }
format.json { render json: @profile.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_profile
@profile = Profile.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def profile_params
params.require(:profile).permit(
:last_name, :first_name,
:avatar,
avatar_attachment_attributes: [:id, :_destroy]
)
end
end
# View
<p id="notice"><%= notice %></p>
<p>
<strong>Avatar:</strong><br />
<% if @profile.avatar.attached? %>
<%= form_for @profile do |f| %>
<%= f.fields_for :avatar_attachment_attributes do |avatar_form| %>
<%= avatar_form.hidden_field :id, value: @profile.avatar_attachment.id %>
<%= avatar_form.hidden_field :_destroy, value: true %>
<% end %>
<%= f.submit "Delete avatar" %>
<% end %>
<% end %>
</p>ActiveStorage comes with a few built-in wizards to help you do basic operations like extracting metadata or generating previews and variants on certain file formats. These helpers delegate the real work to specialized gems and/or system binaries. Therefore, if you want to use them, you must meet the requirements first.
The documentation states:
Extracting previews requires third-party applications, FFmpeg for videos and MuPDF for PDFs, and on macOS also XQuartz and Poppler. These libraries are not provided by Rails. You need to install them yourself to use the built-in previewers. Before installing and using third party software, make sure you understand the licensing implications of doing so.
gem 'image_processing', '~> 1.2' to your Gemfile.Here is a basic example of how to generate a variant:
<%= image_tag profile.avatar.variant(resize_to_limit: [75, 75]) %>Read https://github.com/janko/image_processing for a complete list of possible transformations.
Likewise, if you want a variant to be generated only once and then stored and reused (for performance reasons), use the method #processed :
<%= image_tag profile.avatar.variant(resize_to_limit: [75, 75]) %>What it does: it first checks the existence of the requested variant. If found, it is used, otherwise it is generated, stored and then used.
Tips: use #variable? first to make sure you can create a variant : some_profile.avatar.variable?. Call #variant If MiniMagick is not installed or if the file format does not allow it, an error will be raised.
When working with a video PDF, you can generate a preview:
<%= image_tag(customer.contract.preview(resize: '200x200') %>Tip: use #previewable? first to make sure you can create an overview: some_profile.avatar.previewable?
A beautiful envelope takes care of it: #representation:
<%= image_tag(record.attachment.representation(resize: '500x500') %>Tip: use #representable? First, make sure you can create a variant or a preview: some_profile.avatar.representable?
Do you remember that we started by creating two database tables? These were blobs (information about the attached file) and attachments (a junction table between your models and the blobs). If you need to retrieve information about a downloaded file, you know where to look.
A few examples:
picture, you have to read some_record.image.metadata [:width].some_record.document.content_typeVideo joint, you need to read some_record.video.byte_sizeI advise you to read the official documentation or - even better - the ActiveStorage source code to find an exhaustive list.
Note that depending on how you attached a file (via upload or using #attach), the file may or may not have been scanned. File analysis extracts information such as image dimensions and stores it in blob metadata.
Let's go back to the example of :avatar above. Here's how to attach a local file to your recording:
some_profile.avatar.attach(io: File.open('/path/to/file'), filename: 'avatar.png')In doing so, we should also explicitly ask ActiveStorage to analyze the file and fill in the blob's metadata attribute when required:
some_profile.avatar.attach(io: File.open('/path/to/file'), filename: 'avatar.png')ActiveStorage does not provide a validation mechanism at this time. To add your validations, you will simply need to write custom validations in your template. It would look like this:
class MyModel < ActiveRecord::Base
has_one_attached :image
validate :image_size
private
def image_size
errors.add :image, 'too big' if image.blob.byte_size > 4096
end
endLet's finish this article on this last subject.
ActiveStorage adds scope with_attached_ <attribute> which prevents N+1 requests when loading records and accessing their attachments. In the example of a model of Profile with has_one_attached: avatar, the scope would be With_attached_avatar.
You would use it this way: Profile.with_attached_avatar.find_each {...}
This scope is great, but we are often faced with the situation where we want to list records that actually have attachments. George Claghorn answered the same question in the simplest and clearest way possible: https://github.com/rails/rails/issues/32295#issuecomment-374304126
Here is his code snippet:
# Assuming a model defined like so:
class Post < ApplicationRecord
has_one_attached :image
end
# ...you can join against :image_attachment to select posts having attached images:
Post.joins(:image_attachment).where('published_at >= ?', Time.now)That's it for this article! As always, I recommend that you read the ActiveStorage source code to better understand how it works, extend it and, why not, contribute to it.
github-hosted application example: https://github.com/yoones/demo_activestorage
Thanks for reading!