Related articles

No items found.
The French newsletter for Ruby on Rails developers. Find similar content for free every month in your inbox!
Register
Share:
Blog
>

How to use ActiveStorage in your Rails 5.2+ application

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:

  1. How do you add ActiveStorage to your Ruby on Rails 5.2+ project?
  2. How to choose where to store uploaded documents (on the local drive, on Amazon S3, etc.)
  3. How to make a model have only one attachment (has_one_attached)
  4. How to make a model have multiple attachments (has_many_attached)
  5. How do I check the presence of an attachment, create a link to it, or read the content?
  6. How to destroy attachments
  7. How to do basic manipulations on downloaded files (create variants, previews, read metadata,...)
  8. How to attach a local file (useful in tests and seeds)
  9. How to add validations to uploaded files
  10. How to find records with attachments

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

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:

  • add an attribute to your template/table to allow it to have a single attachment
  • create a related table when you want your model to have multiple attachments.

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.

2. How do I choose where to store the uploaded documents (on the local drive, on Amazon S3, etc.)?

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)

  • How to configure the chosen service (what path, what credentials are required,...).

The list of services is quite simple to understand:

  • Disk : Store files on your local drive
  • 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)
  • 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:

  • one 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.
  • one 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.
  • one 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.

3. How to make a template have only one attachment (Has_One_Attached)

Model side:

  • Step 1: Choose a name for your attachment. Let's say you want to add an image avatar To one Profile in profile.
  • Step 2: Add the following to your model: has_one_attached: avatar

Reminder : 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!

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

Model side:

  • Step 1: Choose a name for your attachment. Let's say you want to add Contracts pdf of contracts to a template of Customer.
  • Step 2: Add the following to your model: has_many_attached:contracts

Controller 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 %>

5. How do I check the presence, link, or reading of the content of an attachment?

Check for the presence of

some_profile.avatar.attached?

Link to

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

Read the contents of the file

binary_data = some_profile.avatar.download

Be careful when doing this on large files stored on the cloud!

6. How to destroy attachments

You can also delete an attachment:

  • synchronously: some_profile.avatar.purge
  • asynchronously: some_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:

  1. The first is to write your own controllers/actions/routes. The advantage is that you can easily add policies and allow/refuse the destruction of a document based on your own constraints.
  2. The other solution is to add 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>

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

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.

Generate a variant of an image

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.

Generate an overview

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?

Let ActiveStorage decide if #variant or #preview should be called

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?

Read the metadata, file size, etc.

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:

  • For attachment width picture, you have to read some_record.image.metadata [:width].
  • For a document attachment content type, you should read some_record.document.content_type
  • For the size of a file Video joint, you need to read some_record.video.byte_size

I 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.

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

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

9. How to add validations to uploaded files

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
end

10. How to find records with attachments

Let'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!