How to use ActiveStorage in your Rails 5.2+ application

Younes Serraj
Younes SerrajMar 22, 2019
How to use ActiveStorage

How to use ActiveStorage

Ruby on Rails, our beloved framework, provides a new standard for file upload. Please welcome ActiveStorage!

This article is a super quick, straight to the point guide to get started with ActiveStorage.

I’ve hosted a working example application on Github for you to try it out-of-the-box. It illustrates most of what this article presents. The link is at the end of the article.


Table of content:

  1. How to add ActiveStorage to your Ruby on Rails 5.2+ project

  2. How to choose where to store uploaded documents (on local disk, on Amazon S3, etc.)

  3. How to make a model have one attachment (has_one_attached)

  4. How to make a model have many attachments (has_many_attached)

  5. How to check the presence of, link to or read the content of an attachment

  6. How to destroy attachments

  7. How to do basic manipulations on uploaded files (create variants, previews, read metadata, …)

  8. How to attach a local file (useful in tests and seeds)

  9. How to add validations on uploaded files

  10. How to find records with attachments


1. How to add ActiveStorage to your Ruby on Rails 5.2+ project

There’s no gem to add to your Gemfile as Rails 5.2 comes with ActiveStorage built in. Simply run rails active_storage:install which will generate a migration file, then run rake db:migrate.

If you read this migration (always be curious!), you see that it adds two tables to your database:

  • active_storage_blobs: this table records blobs which are file-related information (filename, metadata, size, etc.)

  • active_storage_attachments: this is a join table between your application's models and blobs.

So far you probably have been used to:

  • adding an attribute to your model/table to allow it to have a single attachment,

  • creating an associated table when you want your model to have multiple attachments.

ActiveStorage removes these two steps. You don’t have to generate any migration anymore to make your models have one or many 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 blobs. If it's still foggy for you don't worry, we'll get back to this point shortly, it's actually pretty easy to understand.

For now, let’s just focus on the configuration. We generated a migration and migrated the database, we now have to tell ActiveStorage where to store uploaded files.


2. How to choose where to store uploaded documents (on local disk, on Amazon S3, etc.)

First read config/storage.yml. This file allows you to define several storage strategies. Each environment will be assigned a storage strategy.

Here is the default-generated config/storage.yml:

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 basically tells ActiveStorage two things:

  • Which service to use (chose between Disk,S3,GCS,AzureStorage and Mirror)

  • How to configure the chosen service (what path, what credentials if any required, …)

The service list is quite simple to understand:

  • Disk : Store files on your local disk

  • S3 : 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)

  • AzureStorage: Use Microsoft Azure Storage (requirement: add gem 'azure-storage' to your Gemfile)

Then there is MirrorMirror tells ActiveStorage to use both a primary storage strategy and a collection of other strategies to make copies of your uploaded documents. You wanted an easy way to build backups for uploaded documents? Mirror is a nice one.

One more thing about the mirror service: though copies are made, all your queries and downloads will be performed on/from the primary strategy. This is a backup mechanism, not a load balancing one.

So back to config/storage.yml and your storage strategies list.

As in the above example, you might choose to have:

  • a test strategy for when you run your rspec/minitest/whatever. In this strategy, you'll probably want to store uploaded files in Rails.root.join("tmp/storage")so that you can clean them up by running rake tmp:clean.

  • a local strategy for development environment. This would store uploaded files in a non-volatile storage, let's say in Rails.root.join("storage")for instance.

  • an amazon strategy for production environment. This would store uploaded files in an Amazon S3 bucket.

I’m not going to explain each service’s configuration specifics as it is pretty self-explanatory. Just read the examples above and you’re basically done. Oh and, obviously, don’t forget to configure your external services on their respective platforms beforehand (ex: for S3, create a bucket and set the right permissions).

Once you wrote your storage strategies (you can keep the default ones for now), you have to assign a strategy to each environment you run.

Concretely: in each config/environments/*.rb file, set the attribute config.active_storage.service to the strategy you want.

For instance, I usually have in config/environments/development.rb the following line: config.active_storage.service = :local.


3. How to make a model have one attachment (has_one_attached)


Model-side:

  • Step 1: choose a name for your attachment. Let’s say you want to add an avatar image to a Profile model.

  • Step 2: Add to your model the following: has_one_attached :avatar

Reminder: you do not need to add a new column to your database table!

Now you can use some_profile.avatar.attached? to check whether a file is present or not.


Controller-side:

To allow the upload of an avatar, add :avatar to your permitted

params.require(:profile).permit(:some_attribute, :some_other_attribute, :avatar)



View-side:

<%= form.file_field :avatar %>


That’s it!


4. How to make a model have many attachments

(has_many_attached)

Model-side:

  • Step 1: chose a name for your attachment. Let’s say you want to add contracts pdf files to a Customer model.

  • Step 2: Add to your model the following: has_many_attached :contracts


Controller-side:

To allow the upload of new contracts, add contracts: [] to your permitted params:

params.require(:customer).permit(:some_attribute, :yet_another_attribute, contracts: [])

Now you can use some_customer.contracts.attached? to check whether at least one file is present or not.



View-side:

<%= form.file_field :contracts, multiple: true %>



5. How to check the presence of, link to or read the content of an attachment


Check the presence of

some_profile.avatar.attached?


Link to

Since the file’s location is storage-strategy dependent, ActiveStorage provides a helper 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 using rails_blob_url or rails_blob_path:

rails_blob_path(some_profile.avatar, disposition: 'attachment')


Read file content

binary_data = some_profile.avatar.download

Be careful when doing so on big files stored on the cloud!


6. How to destroy attachments

You can destroy an attachment either:

  • synchronously: some_profile.avatar.purge

  • asynchronously: some_profile.avatar.purge_later. This will schedule an ActiveJob to take care of it.

You might also want to permit a user to remove attachments. I can propose two solution:

  1. One is to write your own controllers/actions/routes. The advantage is that you can easily add policies and allow/deny destruction of a document given you own constraints.

  2. The other solution is to add accept_nested_attributes_for. Let me explain this one.

I assume you are accustomed to using accept_nested_attributes_for.

When you add has_many_attached :contracts to a Customer model, ActiveStorage injects has_many :contracts_attachmentsin your model as well.

Read this to know what precisely happens behind the scene: https://github.com/rails/rails/blob/0f57f75008242d1739326fec38791c01852c9aa7/activestorage/lib/active_storage/attached/model.rb

Here’s how you would allow contracts attachments destruction:

# 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>


7. How to do basic manipulations on uploaded files (create variants, previews, read metadata, …)

ActiveStorage comes with a couple of built-in helpers to assist you in doing basic operations such as 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 says:

Extracting previews requires third-party applications, FFmpeg for video and muPDF for PDFs, and on macOS also XQuartz and Poppler. These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you install and use third-party software, make sure you understand the licensing implications of doing so.

  • To generate variants from images, install MiniMagick (http://www.imagemagick.org) on your system then add gem 'image_processing', '~> 1.2'to your Gemfile.

Generate a variant of an image.

Here’s 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 comprehensive list of possible transformations.

Also, if you wish to have a variant generated only once then stored and reused (for performance reasons), use the #processed method:

<%= image_tag profile.avatar.variant(resize_to_limit: [75, 75]) %>

What that does: it first checks for the existence of the requested variant. If found, it is used, otherwise it is generated, stored then used.

Advice: use #variable? first to make sure you can create a variant: some_profile.avatar.variable?. Calling #variant if MiniMagick is not installed or when the file format does not allow it will raise an error.


Generate a preview

When working with a PDF of a video, you can generate a preview:

<%= image_tag(customer.contract.preview(resize: '200x200') %>

Advice: use #previewable? first to make sure you can create a preview: some_profile.avatar.previewable?


Let ActiveStorage decide if it is #variant or #preview that should be called

A nice wrapper takes care of this: #representation:

<%= image_tag(record.attachment.representation(resize: '500x500') %>

Advice: use #representable? first to make sure you can create either a variant or a preview: some_profile.avatar.representable?


Read the metadata, file size, etc.

Do you remember we started by creating two database tables? They were blobs (information about the attached file) and attachments (a join table between your models and blobs). If you need to retrieve information about an uploaded file, you know where to look.

A couple of examples:

  • For an image attachment width, you would read some_record.image.metadata[:width].

  • For a document attachment content type, you would read some_record.document.content_type

  • For a video attachment file size, you would read some_record.video.byte_size

I advise you read the official documentation or -even better- the source code of ActiveStorage to find an exhaustive list.

Note that depending on how you attached a file (via upload or using #attach), the file might or might not have been analyzed. File analyze extracts informations like image dimensions and saves them in blob metadata.


8. How to attach a local file (useful in tests)

Back to the above :avatar example. This is how to attach a local file to your record:

some_profile.avatar.attach(io: File.open('/path/to/file'), filename: 'avatar.png')

Doing so, we also need to explicitly ask ActiveStorage to analyze the file and populate the blob’s metadata attribute when need be:

some_profile.avatar.attach(io: File.open('/path/to/file'), filename: 'avatar.png')


9. How to add validations on uploaded files

ActiveStorage does not provide a validation mechanism as of now. To add your validations, you will simply write custom validations in your model. It would somehow 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
end


10. How to find records with attachments

Let’s end this article on this final topic.

ActiveStorage adds a with_attached_<attribute> scope that prevents N+1 queries when loading records and accessing their attachments. In the example of a Profile model 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 often face the situation where we want to list records that actually have attachments. George Claghorn answered this very question in the most simple and clearest way: https://github.com/rails/rails/issues/32295#issuecomment-374304126

Here is his snippet code:

# 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)


This is it for this article! As always, I advise you read the ActiveStorage source code to understand better how it works, maybe extend it and why not even contribute to it.

Github-hosted example application: https://github.com/yoones/demo_activestorage

Thanks for reading!

Partager
Younes Serraj
Younes SerrajMar 22, 2019

Capsens' blog

Capsens is an agency specialized in the development of fintech solutions. We love startups, scrum methodology, Ruby and React.

Ruby Biscuit

The french newsletter for Ruby on Rails developers.
Get similar content for free every month in your mailbox!
Subscribe