Rails 5.2: Active Storage and beyond

Cover for Rails 5.2: Active Storage and beyond

New ways to handle file uploads, share credentials with your team, set up Content Security Policy, even start your application—we are looking at what’s new in Rails 5.2 and focus on Active Storage with a step-by-step introduction to the new framework.

Even though we did not get it as a promised New Year present and were just told to wait another month for the official release, Rails 5.2 was assigned RC1 status and is now considered stable. So we might as well start unwrapping! You can install Rails 5.2 today by running gem install rails --prerelease.

Out of all new shiny things that the last major update before Rails 6 brings, Active Storage stands out the most. For the first time in Rails history, we get a built-in solution for handling file uploads in our projects. According to DHH, Active Storage was extracted from Basecamp 3, so it claims to be a framework “born from production”.

We will talk about Active Storage first, and then, if you bear with us till the end of this guide, we will go into other Rails 5.2 features.

With things attached

Disclaimer: we will not go into comparing Active Storage with existing solutions, be it CarrierWave, Paperclip or Shrine, but rather try to make a beginner-friendly introduction to the framework as we get to know it ourselves.

Enabling Active Storage in your application starts with a Rake task: running rails active_storage:install in the command line will add a new migration to your db/migrate folder. Once executed, it creates two tables that Active Storage needs to deliver on its promises: active_storage_attachments and active_storage_blobs. Here is what they do, according to framework’s README:

“Active Storage uses polymorphic associations via the Attachment join model, which then connects to the actual Blob. Blob models store attachment metadata (filename, content-type, etc.), and their identifier key in the storage service.”

This approach sets Active Storage apart from the competition. Paperclip, Carrierwave, Shrine—all these popular solutions require you to add columns to existing models. The only widely used gem that relies on virtual attributes to handle attachments is Attachinary, a ridiculously easy-to-use proprietary solution that is locked to Cloudinary’s storage.

Active Storage, it seems, takes the same direction, but allows you to choose where to keep your files: be it your hardware or popular cloud providers. Amazon S3, Google Cloud Storage, and Microsoft Azure Storage are supported out of the box.

Sometimes it is OK to scaffold

Time to see Active Storage in action. Assuming you have already created a new application, comment out the jbuilder gem in the Gemfile and run bundle install, as it will give us a cleaner scaffold. After all, we only want to take a peek at Active Storage and not spend time building full CRUD just to prove our originality:

$ rails g scaffold post title:string content:text

Let’s start by attaching a single image to our post. Add a single line of code to your model definition:

# app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

To get started with Active Storage, you need to change only three things in your code, and we have just taken care of the first one.

Now let’s take a look at the posts_controller.rb that was kindly created for us by a scaffold generator. The only thing we need to do here is to white-list an image parameter inside the idiomatic post_params method and make it look like this:

# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, :image)
end

By the way, if you want to attach a file to the existing model somewhere else in the controller code, here’s how you do it:

@post.image.attach(params[:image])

Note: if you are using create or update on a resource in your controller action and pass attachment as a permitted parameter, the line above is not needed and will break things. Some earlier tutorials out there require you to attach a file explicitly, but that is not the case anymore.

Now for the views: in a generated _form.html.erb add a file_field right above the submit button:

<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :image %>
  <%= form.file_field :image %>
</div>

And in order to display our image:

<!-- app/views/posts/show.html.erb -->
<% if @post.image.attached? %>
<!-- @post.image.present? will always return true, use attached? to check presence -->
  <p>
    <strong>Image:</strong>
    <br>
    <%= image_tag @post.image %>
  </p>
<% end %>

That is it! Rails handles all the nitty-gritty details of multipart form upload for you. Let’s check if it works. Launch the server with rails s and go to localhost:3000/posts/new in your browser. Pick any image you like and create a post.

First post with Active Storage

Smoke test for Active Storage

Editing this post and changing an image will work right away too, just give it a try. Voila, file uploads are enabled in your application!

To sum up, here’s all we did:

  • Model: We called has_one_attached method in the model definition with a symbol that will become a virtual attribute on each instance of our model. We chose to name it :image, but it could have been :anything_you_want.
  • Controller: We white-listed image as a permitted parameter.
  • Views: We added a file_field to our form and displayed an uploaded image in the image_tag.

Now, let’s see what had happened behind the scenes when we submitted that form. The first thing we notice when we peek into the server logs—every SQL statement is now accompanied by a reference to a line of code that generated it. Previously, that was a job of Query Trace gem; now this feature is baked into Rails. If you are not happy with the output (there is a bug that affects certain rbenv setups), set config.active_record.verbose_query_logs option to false inside development.rb.

Log for a POST request

POSTing a form with attachment

We can see that Rails processes the form, stores the received file on disk, encodes its location as a key, references that key in the active_storage_blobs table, creates a new record in the posts table and then associates a Post with a Blob through active_storage_attachments.

And here is what’s happening when the show action of our PostsController is invoked through GET:

Log for a GET request

GETting upload result

One request turns into three: ActiveStorage::BlobsController and ActiveStorage::DiskController are both involved in serving a file. This way, public URL of your image is always decoupled from its actual location. If you are using a cloud service—BlobsController will redirect to a correct signed URL in your cloud.

Let’s see what we can now do with our Post instance inside the rails console:

Rails console

Toying with a record in Rails console

Note that to generate a URL for an attachment we need to call service_url and not url. View helpers like url_for and image_tag know how to do this for us, so you will rarely need to call that method explicitly.

Solving N+1

Given that rendering one attachment in a view results in at least three database queries (one to the parent model, one to the active_storage_attachments table and another to active_storage_blobs)—should we be worried when we iterate over a collection of Active Record objects that all have attached files? Let’s find out! Modify your index.html.erb to show an image for each post or at least print out its filename (post.image.filename will also trigger all the queries), refresh /posts in your browser and take a look at the log:

N+1 in Active Storage

N+1 at its finest

Do you see the problem? N+1 hydra raises its ugly head again. Luckily, Active Storage provides a solution: it generates a scope with_attached_image (or with_attached_your_attachment_name) which includes the associated blobs. All you need to do is to change @posts = Post.all in your PostsController#index to @posts = Post.with_attached_image. Here’s the result:

N+1 problem solved

Problem solved!

Nice! But what if you want to use eager_load or preload instead of includes because you do not trust Active Record to always make the right choice for you? Here’s how you do it:

class Post < ApplicationRecord
  scope :with_eager_loaded_image, -> { eager_load(image_attachment: :blob) }
  scope :with_preloaded_image, -> { preload(image_attachment: :blob) }
end

Note that we use nested preloading here: we load blobs through attachments (image_attachment is a name of the association added by Active Storage in this particular case, if you choose to name your attachment differently, association name will change too).

Multiple attachments

Setting up your CRUD for multiple attachments is also a piece of cake.

  • Model:
# app/models/post.rb
class Post < ApplicationRecord
  has_many_attached :images
  # Note that implicit association has a plural form in this case
  scope :with_eager_loaded_images, -> { eager_load(images_attachments: :blob) }
end
  • Controller:
# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, images: [])
end
  • Views:
<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true %>
</div>

<!-- app/views/posts/show.html.erb -->
<% if @post.images.attached? %>
<p>
  <strong>Images:</strong>
  <br>
  <% @post.images.each do |image| %>
    <%= image_tag(image) %>
  <% end %>
</p>
<% end %>

If you want to delete an attachment (or all of them), there are two methods for that: purge and purge_later. The second one will handle file deletion in the background through the built-in ActiveStorage::PurgeJob. This asynchronous purging will also be called by default if you delete the parent model.

Image variants with ImageMagick

Currently, our images are displayed exactly as a user uploaded them, and that is not what we usually want. Active Storage supports image transformations with ImageMagick. All you need to do is to enable a mini_magick gem that is already added to your Gemfile (and commented out). It allows you to perform all ImageMagick’s transformations. For instance:

<%= image_tag image.variant(resize: "500x500", monochrome: true) %>

It will create a URL for that specific variant of that specific blob, but the transformation itself will not be handled by ActiveStorage::VariantsController until the image is first requested by a browser. Active Storage tries to delay a potentially expensive operation: the original blob needs to be downloaded from the service, transformed in your server’s memory and uploaded to the service again.

If you want to first process the file and then get the URL you can call image.variant(resize: "100x100").processed.service_url. It will check if the transformation for that particular variant had been performed before—if so, it would not be repeated.

You can also generate previews for videos (with ffmpeg) and PDFs (with mutool)—but it is your job to ensure those tools are available, as their libraries are not provided by Rails.

After you have enabled ImageMagick, all your incoming files will be automatically (and asynchronously) analyzed for metadata; you can call post.image.metadata to get a hash that will look like this: {"width"=>1200, "height"=>700, "analyzed"=>true}.

If you prefer to treat your images with something other than ImageMagick—you are out of luck, Active Storage currently does not have an official way to work with other image processing libraries or tools like imgproxy.

No more secrets

Well, it all seemed to work perfectly fine locally, but didn’t we have to configure something? Not really, in a familiar Rails “omakase” fashion, a basic configuration was already done for you in config/storage.yml. Let’s take a peek:

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

In development.rb you already have config.active_storage.service = :local set, so Rails knows to use your disk when you work with Active Storage on localhost. There are also configurations for Amazon’s, Google’s and Microsoft’s cloud services, all commented out.

Putting your keys directly into storage.yml would be a bad idea, as you would probably want to have this file in your source control. Rails thought ahead and assumed your keys are stored inside Rails.application.credentials. Wait, but wasn’t it called secrets? Yes, it was, but, in the words of DHH himself:

“The combination of config/secrets.yml, config/secrets.yml.enc, and SECRET_BASE_KEY is confusing. It’s not clear what you should be putting in these secrets and whether the SECRET_BASE_KEY is related to the setup in general.”

To put an end to this confusion, Rails 5.2 introduces a new concept called Credentials

They are stored inside config/credentials.yml.enc in an encrypted form, so they are safe to be committed to source control. No more messing around with environment variables or synchronizing modified keys across the team!

The file that should not be tracked by Git under any circumstances (and is already listed in .gitignore for new Rails 5.2 projects) is config/master.key. It contains the autogenerated key that allows to decrypt your credentials. As documentation warns us:

“Don’t lose this master key! Put it in a password manager your team can access. Should you lose it no one, including you, will be able to access any encrypted credentials.”

So how do you edit your credentials, if the file containing them is always encrypted? Rails 5.2 has a new task for that: rails credentials:edit. This command will open your default editor with a plain text file where you can put your keys in key_name: key_value format. YAML nesting is also permitted. You can then access your credentials with Rails.application.credentials.key_name or, if you use nested keys, with Rails.application.credentials.dig(:section_name, :nested_key_name). Once you save and close the temporary file, its contents will be encoded into config/credentials.yml.enc. You can also print your keys to Terminal with rails credentials:show.

Now, provided that all members of your team have the same master.key file, you can safely collaborate through Git and not be afraid of exposing sensitive information ever again. For production, you will need to set a single RAILS_MASTER_KEY environment variable.

Note: In order to get credentials to work with an external editor like Atom, you need to call the task with EDITOR="atom --wait" credentials:edit. Or use a shell editor, that may be more suitable for quickly editing a few keys: EDITOR=vi credentials:edit.

Active Storage in the cloud

For a simple demonstration, we will use Amazon S3. Obviously, you will need a bucket with public read access. Once you have it, the rest is easy as 1-2-3:

  1. Inside storage.yml remove comments from the amazon: section.
  2. In the development.rb switch the default service to S3 with config.active_storage.service = :amazon.
  3. Type rails credentials:edit in your console and put your keys in the following format:
aws:
  access_key_id: 123
  secret_access_key: 456

That’s it! All uploaded files and their variants are now automatically handled through Amazon S3. post.image.service_url will now generate a signed URL for your bucket instance.

Direct Upload

Last summer, while Active Storage was still in active development, developer Fabio Akita has expressed his concerns for handling large files when your Active Storage-enabled project is hosted on a platform with an ephemeral file system such as Heroku. In a Twitter thread Rails creator DHH was convinced to implement a Direct Upload to the cloud from the browser, bypassing application’s backend completely—similar to what Attachinary has implemented to send files to Cloudinary storage.

Rails Guides already have excellent coverage of a Direct Upload feature along with JavaScript code snippets (finally, ES6 instead of CoffeeScript!).

Here is how quickly to test it out (steps are shown with Webpacker in mind, so make sure you have run rails webpacker:install for your app):

$ yarn add activestorage
// app/javascript/packs/application.js
import * as ActiveStorage from "activestorage";
import "../utils/direct_uploads.js"

ActiveStorage.start();
// app/javascript/utils/direct_uploads.js
// Create this folder and this file, then cut and paste code from
// http://edgeguides.rubyonrails.org/active_storage_overview.html#example
<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true, direct_upload: true %>
</div>

direct_upload: true option generates the following HTML for the file field:

<input multiple="multiple" data-direct-upload-url="http://localhost:3000/rails/active_storage/direct_uploads" name="post[images][]" id="images" type="file">

Sample JavaScript code demonstrates the use of Direct Upload JS events that allow you to follow the upload cycle and react accordingly in the UI, by updating a progress bar, for instance.

Put sample CSS code from the same Rails Guides section
inside the direct_uploads.css file that you can create in the app/assets/stylesheets folder. Now, when you restart your server and head to a new post page, you will be able to choose a large file and witness that your server is not blocked on a long task—the upload is happening entirely in XHR with a dynamically updated progress bar:

Direct Upload from Chrome

Uploading files directly from a browser

Note: At the time of this writing, this feature did not work in Firefox and resulted in XML Parsing Error when used with S3.

If you are using S3, you have to open up your CORS settings in your bucket’s “Permissions” tab. Note the following settings are only good for a quick test in development, don’t leave it wide-open in production!

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Mirror, mirror on the wall

Yet another big feature of Active Storage is mirroring, it allows you to keep files in sync across several cloud storage providers for redundancy or during the migration between clouds. For instance, in storage.yml:

production:
  service: Mirror
  primary: local
  mirrors:
    - amazon
    - google

Then in production.rb you can set your service to :production. This setup will store uploaded files locally and back them up to Amazon S3 and Google Cloud Storage at the same time. Once the file is deleted, it will be removed from both clouds too.

That was all we could say about Active Storage at this point; it still has to be battle-proven in production so we can provide a deeper insight.

Now, let’s take a look at few other notable Rails 5.2 features.

DSL for Content-Security-Policy header

Security matters, isn’t it? Now it is time to make our applications even more secure. CSP is designed to restrict “inbound” network requests (i.e., what can be loaded on your page) but as a side-effect it also restricts outgoing requests from the browser, thus preventing evil hackers from hacking.

Now it is easy to configure CSP in your Rail application either globally:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |p|
  # allow everything from the current hostname by default (only secured)
  p.default_src :self, :https
  # allow loading fonts and images from data-uri
  p.font_src    :self, :https, :data
  # don't forget to add your cloud hostname for ActiveStorage assets
  p.img_src     :self, :https, :data, "cloudfront.example.com"
  # disallow <object> tags (Good-bye Flash!)
  p.object_src  :none
  # allow inline <style> (remove :unsafe_inline if you don't want it)
  p.style_src   :self, :https, :unsafe_inline
end

Or per-controller (even dynamically):

class PostsController < ApplicationController
  # Extends/overrides global policy
  content_security_policy do |p|
    # set user-specific domain as base for the policy
    p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

There is one major undocumented (at the time of this writing) caveat though. If you are using Webpacker and webpack-dev-server, you will have to update CSP for the development environment and permit connections to http://localhost:3035 and ws://localhost:3035, otherwise Rails will block web socket connections needed for Webpack’s hot reload functionality. Read more in this issue. Thanks to Nick Savrov for discovering this:

# You need to configure your CSP like so if you use Webpacker
Rails.application.config.content_security_policy do |p|
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.style_src   :self, :https, :unsafe_inline

  if Rails.env.development?
    p.script_src :self, :https, :unsafe_eval
    p.default_src :self, :https, :unsafe_eval
    p.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035'
  else
    p.script_src :self, :https
    p.default_src :self, :https
  end
end

This is also a reason why Rails team has decided to disable CSP configuration by default.

Current everything

Have you ever wondered how to access current_user in your model? According to StackOverflow, about a third of all questions containing “current_user” are about using this method in a model.

The situation is going to change in Rails 5.2: now we can add a magic Current singleton which acts like a global store accessible from anywhere inside your app:

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

All you have to then is to set the user somewhere in your controller to make it accessible in models, jobs, mailers, wherever:

class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

# and now in your model
class Post < ApplicationRecord
  # You don't have to specify the user when creating a post,
  # the current one would be used by default
  belongs_to :user, default: -> { Current.user }
end

This idea is not new; we already have a request_store gem by Steve Klabnik that does the same for any Rack app.

Note: You may say: “This feature breaks the Separation of Concerns principle!” Yes, it does. Don’t use it if it feels wrong.

HTTP/2 Early Hints

Early Hints is an HTTP/2 feature that allows you to make browsers pre-download assets before encountering them within the page HTML. This way, we can reduce page loading time by leveraging the ability of HTTP/2 to pipeline requests.

All you have to do is to run your Puma server with --early_hints flag and use an HTTP/2-compliant proxy in front of it (such as h2o).

Read more about Early Hints in this post by Eileen Uchitelle who authored the feature.

Bootsnap

Working with a feature-heavy framework like Rails comes with some trade-offs, boot time being one of them. A large monolithic application can take a minute or two to start (or run a task).

Now on top of Spring, which keeps Rails app pre-booted between runs, we get Bootsnapa piece of magic a tool by Shopify that speeds up loading Ruby and YAML files, resulting in 2 to 4 times faster cold start. It is included in your Gemfile by default.

Bootsnap comes especially handy when you develop with Docker (where setting up Spring is not so trivial) or deal with CI servers.

Rails 5.2 is brand new, so we have not had a chance to test it out in production yet. Feel free to share your experiences on Twitter by referencing our account or contact us directly. Big thanks to Mike Gunderloy for his post on Active Storage that helped us a lot while we prepared this article.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.