Early look at Rails ActiveStorage

I took an early peek at the new Active Storage project in Rails 5.2. It is a built-in solution to handling uploads, something that is all to common in web applications. Also something that is covered by quite a few gems, CarrierWave, PaperClip and Refile for example. These are just the ones I know off the top of my head.

At times I have also handled uploads without gems, it’s not that difficult and didn’t warrant the inclusion of another gem for those projects.

I’m surprised it took this long for it to be incorporated into Rails. Its batteries included approach is well known and a big factor in its success, and uploads surely is a ‘battery’ for many web apps. Anyway, I was intrigued and wanted to check it out.

First step was to get the latest rails from github, I installed rails.

rails new active-storage-example

And changed the Gemfile to use the latest master

gem 'rails', github: 'rails/rails'
gem 'arel', github: 'rails/arel'

Arel also had to be included or running bundle install triggered some errors.

Next was installing activestorage, I used the following command: rails activestorage:install This creates a migration for ActiveStorage. This is necessary as there are two tables created to keep track of the uploads. There is an active_storage_blobs table, it keeps track of the files/images etc. you upload. To map those back to the models in your application the other table is active_storage_attachments. It has the polymorphic relationship between the blob and the other models in your app. I’m not a fan of polymorphic relations but it is convenient in this case.

To progress we first create a scaffold for users: rails g resource user username:string email:string

We also have to get the database in order

rails db:create
rails db:migrate

A new configuration file is needed at config/storage.yml. For now we will store the files on disk but there are existing adapters for S3, Azure and Google Cloud Storage. A mirror service is also a possibility, allowing you to store files in multiple places.

#config/storage.yml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

We named the storage option local, and we’ll need to tell Active Storage to use that. Add the following line to config/application.rb

    config.active_storage.service = :local

Note how easy it’ll be to store locally in development, and in the cloud when you move to production.

The model we created earlier, needs to be told it has many attached images.

# app/models/user.rb
class User < ApplicationRecord
  has_many_attached :images

  # When a user may only have one image use
  # has_one_attached :image
end

The scaffold created a form to create users, it needs to be changed to support the uploads’

<!-- > app/views/users/_form.html.erb -->
  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email, id: :user_email %>
  </div>

  <!-- Added -->
  <div class="field">
    <%= form.label :images %>
    <%= form.file_field :images, multiple: true %>
  </div>
  <!-- end of added code -->

  <div class="actions">
    <%= form.submit %>
  </div>

You’ll notice that the file field has multiple set to true so we can upload multiple files at a time.

The controller also needs to support the uploads. Change the #create method of the users_controllerto the following

# app/controllers/users_controller.rb
  def create
    @user = User.create! (user_params)
    @user.images.attach(params[:user][:images])

    respond_to do |format|
      if @user.persisted?
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

We need to persist the user model by using User#create! before attaching the uploads because otherwise they won’t know where to attach them to. Caution that this code is probably not handling errors all to well, it’s just a POC.

We’re gonna add the following to the users#show view.

<!-- app/views/users/show.html.erb -->
<%  @user.images.each do |image| %>
  <%= image_tag image.variant(resize: "100x100") %>
<% end %>

It’ll iterate over each attached image and show a resized variant of it of 100x100 pixels. The resizing is done by a controller in the ActiveStorage engine. It’ll lazily resize the image and use a cached version if it is called before.

If you run this it will trigger an error, that is because the resizing is done by the Mini Magick gem, a wrapper around Imagemagick. In fact, the transformations supported by the variant are also those supported by mini magick.

Add gem 'mini_magick to your Gemfile and run bundle install to fix it.

Now, if you run rails server and go to http://localhost:3000/users/new there will be a form to add users with files. Upload a couple of images and go to users/1 and they’ll be resized.

If you look at the storage folder in your app you’ll see the uploaded files, and there will be some entries in your active_storage_ tables in the database.

All in all, a very clean solution to uploading in Rails. Certainly something I would’ve used in some past projects. It is not done yet, you can follow the work being done here [https://github.com/rails/rails/tree/master/activestorage]

I almost forgot, there is also support for direct uploads to the cloud, even more awesome.

Photo by Maarten van den Heuvel on Unsplash