Rails 5.2 and Active Storage - the new approach to file uploads

Rails 5.2 and Active Storage - attach files, the modern way.

What is Active Storage in Ruby on Rails?

ActiveStorage, a built-in way for uploading files in web applications was among the features that came with Rails 5.2. The files are uploaded to cloud storage services like Amazon S3, Google Cloud Storage or Microsoft Azure Storage and then attached to Active Record objects in the app. That means Rails developers no longer have to use third-party libraries like CarrierWave for example. Let's check out what needs to be done when adapting your project to ActiveStorage

Adapting your Rails project to Active Storage

If you are updating your existing Rails project, right after upgrading Rails to 5.2 you need to run command rails active_storage:install. This will create new migrations for ActiveStorage to use - more precisely, it will create active_storage_blobs and active_storage_attachments tables in your database.

# db/migrate/20180407102457_create_active_storage_tables.active_storage.rb
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true

    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true

So why do we need both of them anyway?

ActiveStorage will use these tables to keep track of your uploads via polymorphic associations. That's right - no more creating additional columns in your models for images and files. Blobs are records that contain the metadata of the file (such as content type, size, filename etc.) and an encoded key which points to the file location on the service (be it local or cloud). Attachments associates your models with blobs.

After migration, you need to add associations to attachments in your model:

# app/models/blog_post.rb
class BlogPost < ApplicationRecord
  has_one_attached :main_picture # one-to-one relationship
  has_many_attached :uploads # one-to-many relationship

Here I have added has_one_attached :main_picture so that my BlogPost model has one main image, and has_many_attached :uploads so I can upload many images / documents to it. The next step is to permit those attributes in controller:

# app/controllers/blog_posts_controller.rb
def blog_post_params
  params.require(:blog_post).permit(:title, :content, :main_picture, uploads: [])

For has_one association we permit a single parameter, and for has_many we permit an array. And that's pretty much it - with ActiveStorage it's so simple to add images to your existing models. Again, you don't need to create uploaders, mount them, add additional columns in your model and all that stuff.


When you install ActiveStorage, it will also create a config file storage.yml

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

  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 ]

You can set your root directory for local and test services, as well as set your keys to Amazon S3, Google Cloud Service, Microsoft Azure. You still need gems for those services to work. Keep in mind that in Rails 5.2 we use Credentials instead of well known secrets. Also ActiveStorage provides mirror service - it keeps your uploads in sync across all mirrors, so you don't need to worry about downtime on one of the services.

Remember to set your service in environment file for the option you choose, for example:

# config/environments/development.rb
config.active_storage.service = :amazon

Active Storage Tips and Tricks

In forms, we use normal file_field for our uploads. Add multiple: true when you use has_many_attached!

-# app/views/blog_posts/_form.haml
  = form.label :main_picture
  = form.file_field :main_picture
  = form.label :uploads
  = form.file_field :uploads, multiple: true # for multiple uploads

It gets a little trickier when showing the uploads to user. ActiveStorage allows to upload any type of files - at a time of writing this, there are no built-in validations - but you can still create one yourself!:

# app/models/blog_post.rb
validate :main_picture_format


def main_picture_format
  return unless main_picture.attached?
  return if main_picture.blob.content_type.start_with? 'image/'
  errors.add(:main_picture, 'needs to be an image')

When showing uploads, we have few possibilities:

-# app/views/blog_posts/show.haml
  %strong Title:
  = @blog_post.title
- if @blog_post.main_picture.attached?
    %strong Image:
    = image_tag @blog_post.main_picture.variant(resize: '700x700') # on-demand processing
    = image_tag @blog_post.main_picture.variant(resize: '700x700').processed.service_url # process then fetch the image
  %strong Content:
  = @blog_post.content
- if @blog_post.uploads.attached?
    %strong Uploads:
    - @blog_post.uploads.each do |upload|
      - if upload.representable?
        = link_to image_tag(upload.representation(resize: '500x500')), upload # opens in new browser tab
        = link_to image_tag(upload.representation(resize: '500x500')), rails_blob_path(upload, disposition: :attachment) # downloads file
      - elsif upload.image?
        = link_to image_tag(upload, width: 500), upload
      - else
        = link_to upload.filename, upload

Let's talk about the options:

1) if the upload can be changed / transformed (it's an image), we can use MiniMagic to process it (ActiveStorage only supports MiniMagic as of now). To check if upload can be processed, we can call upload.variable?. To use MiniMagic's magic :), we must call upload.variant method with transformation which should be applied (resize, trim, flip etc.). So it looks like this upload.variant(resize: '500x500')

One thing to note - .svg files are images, but they are not variable images - MiniMagick cannot transform them, so when you try to apply variant to them, you will get ActiveStorage::InvariableError! To apply some configuration, you can call image_tag with parameters like: image_tag(@blog_post.svg_image, width: 200)

2) if the upload can be previewed (it's a PDF or a video), simply call upload.previewable?. The preview will be the first page of the document or the first frame of the video. Keep in mind that to use previews, you need to install third-party libraries as ActiveStorage does not provide any, but depends on them - for PDFs it's Poppler or mupdf, and for videos it's ffmpeg.

To make it more compact, ActiveStorage has combined these two modules into one called ActiveStorage::Representable, so you can simply call upload.representation(resize: '500x500') and it will return ActiveStorage::Variant for variable blob, and ActiveStorage::Preview for previewable one.

One thing with variants is that we have 2 choices with them: we can make variants on-demand / lazy transform by calling upload.variant(resize: '500x500x') - this will issue transformation when the image is requested by browser or calling upload.variant(resize: '500x500x').processed.service_url to force transformation - just keep in mind that the entire file needs to be downloaded, processed, uploaded to the service and then return itself again But don't worry - ActiveStorage first checks if variant exists on the service, and if so, returns it instead of transforming again.

To allow users to download file, we use Rails new helper rails_blob_path with disposition: :attachment - it will start downloading file after clicking, instead of opening it in new tab: = link_to image_tag(upload.representation(resize: '500x500')), rails_blob_path(upload, disposition: :attachment)

If you want to attach file in custom controller method or somewhere else in your code, you can call @blog_post.main_picture.attach(params[:main_picture]).

Having multiple files attached to your model you will get N+1 queries accessing them - referring one attached items calls your model controllers, ActiveStorage::Attachment to load polymorphic association and finally ActiveStorage::Blob to load the file itself. To avoid that, we can use built-in scope "with_attached_#{attachment_name}" which will include attached blobs in our query, so instead of BlogPost.all, we use BlogPost.with_attached_uploads.

To delete blob, you can use @blog_post.main_picture.purge which will synchronously delete attachment and resource files or @blog_post.main_picture.purge_later to delete attachment in the background via ActiveJob. You also do not need to specify dependant: :destroy with association - it will automatically delete attachment via purge_later method when the parent model is destroyed. If you want to make many records-one blob relationship, you need to specify has_one/many_attached :attachment, dependant: false to prevent destroying blob when parent model is destroyed

Variants and optimization

As of now, ActiveStorage does not provide any built-in methods for image processing despite using MiniMagick for transformations. So to have the same functionality, as for example CarrierWave offers, you need to do some work. I created lib called Uploads where I keep all my code for various resizes based on CarrierWave and optimization based on Google's PageSpeed documentation for image optimization.

# lib/uploads.rb
class Uploads
  class << self
    def jpeg?(blob)
      blob.content_type.include? 'jpeg'

    def optimize
        strip: true

    def optimize_jpeg
        strip: true,
        'sampling-factor': '4:2:0',
        quality: '85',
        interlace: 'JPEG',
        colorspace: 'sRGB'

    def optimize_hash(blob)
      return optimize_jpeg if jpeg? blob

    def resize_to_limit(width:, height:, blob:)
        resize: "#{width}x#{height}>"

    def resize_to_fit(width:, height:, blob:)
        resize: "#{width}x#{height}"

    def resize_to_fill(width:, height:, blob:, gravity: 'Center')
      blob.analyze unless blob.analyzed?

      cols = blob.metadata[:width].to_f
      rows = blob.metadata[:height].to_f
      if width != cols || height != rows
        scale_x = width / cols
        scale_y = height / rows
        if scale_x >= scale_y
          cols = (scale_x * (cols + 0.5)).round
          resize = cols.to_s
          rows = (scale_y * (rows + 0.5)).round
          resize = "x#{rows}"

        resize: resize,
        gravity: gravity,
        background: 'rgba(255,255,255,0.0)',
        extent: cols != width || rows != height ? "#{width}x#{height}" : ''

    def resize_and_pad(width:, height:, blob:, background: :transparent, gravity: 'Center')
        thumbnail: "#{width}x#{height}>",
        background: background == :transparent ? 'rgba(255, 255, 255, 0.0)' : background,
        gravity: gravity,
        extent: "#{width}x#{height}"

After that, what is left to do is to make new instance method for model. I made one for my BlogPost model like this:

# app/models/blog_post.rb
def main_picture_header_variant
  variation = 500, height: 200, blob: main_picture.blob)), variation)

If you want to do something similar, do not forget to require your lib in your model by adding require 'uploads' before class definition.

# app/models/blog_post.rb
require 'uploads'

class BlogPost < ApplicationRecord


This method allows me to just simply call @blog_post.main_picture_header_variant to fetch that particular variant.

Rails 6.0 released!

Hey, have you heard? After two years of development and thousands of commits, Ruby on Rails 6.0 is finally there. What should you know about it and how can it make your web application better? Read an article on our blog discussing the framework's new features, such as Action Mailbox, Action Text, parallel tests, support for multiple databases, Zeitwerk and others.

Photo from:

Recommended reads

Start a project with your dedicated software house


Contact us

* Required fields

Elo Mordo!Elo Mordo!