Active Storage is still moving along at a fast clip, so I figured it would be good to update my test code. You can find all the examples here in my repo at https://github.com/ffmike/activestorage_sample

A Note On Code Stability

It isn't. Active Storage has been merged to the main Rails project now, but it's not yet merged to master. Expect further changes and improvements, as well as potentially broken things.

That said, I'll try to keep this article & the associated samples up-to-date. No promises though.

Change History

  • 8 August 2017

    • Switch to master branch of Rails
    • Update name of Azure service
  • 2 August 2017

    • Switch to Rails with merged Active Storage, remove separate Active Storage gem.
    • Switched to using `url_for(document.url)` in show. See https://github.com/rails/activestorage/commit/d0e90b4a9dc1accd4f1044fde0dd9a347cd0afcf and future plans at https://github.com/rails/activestorage/issues/77 .
    • Added Microsoft Azure sample.
    • Added direct upload sample.
  • 24 July 2017

    • Updated database schema. If you used an earlier version of the samples, you'll need to drop & recreate the database.
    • Replaced .url with .service_url.

A New Edge Rails Application

Start by spinning up a simple application with edge rails:

1. Create a new directory and use whatever you like (rvm, rbenv, chruby) to give it a fresh Ruby environment. I used Ruby 2.4.1 in my testing.

2. Install bundler:

  gem install bundler

3. Create a simple Gemfile in your new directory:

  source 'https://rubygems.org'
  gem 'rails', git: 'https://github.com/rails/rails.git'

4. Install Rails and everything it drags in:

  bundle install

5. Create a new Rails application in the current directory, using the version of Rails you just installed and overwriting the Gemfile:

  bundle exec rails new . --dev --force

6. Create and start the simplest possible application:

  bundle exec rails g scaffold user name:string
  bundle exec rails db:create
  bundle exec rails db:migrate
  bundle exec rails s

You should now be able to go to http://localhost:3000/users to create and update users. Yay!

Active Storage with Local File Storage

Let's start by making sure that Active Storage is working without getting the cloud involved:

1. Create a new branch of code:

  git checkout -b local

2. Install Active Storage to your application:

bundle exec rails activestorage:install

This will create the storage and tmp/storage directories in your application, copy a default configuration file to config/storage.yml, and create a new migration. The migration builds the active_storage_blobs and active_storage_attachments tables in your database.

3. Update the database schema:

bundle exec rails db:migrate

4. Set up the development environment to use local storage by adding a line to your development.rb file:

config.active_storage.service = :local

5. Tell your User model that it has some attached files:

class User < ApplicationRecord
  has_one_attached :avatar
  has_many_attached :documents
  ...
end

6. Comment out the amazon, google, microsoft, and mirror sections from the config/storage.yml file. Otherwise, your server won't start, because it will be looking for keys and files that don't exist.

7. Add input fields to app/views/users/_form.html.erb:

  <div class="field">
    <%= form.label :avatar %>
    <%= form.file_field :avatar %>
  </div>

  <div class="field">
    <%= form.label :documents %>
    <%= form.file_field :documents, multiple: true %>
  </div>

8. Add controls to app/views/users/show.html.erb to display the data

<p>
  <%= image_tag(url_for(@user.avatar)) %>
<p>

<p>
  <strong>Documents:</strong>
  <ul>
    <% @user.documents.each do |document| %>
      <li><%= link_to document.blob.filename, url_for(document) %></li>
    <% end %>
  </ul>
<p>

9. Update your users controller to attach the files:

  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)
    avatar = params[:user][:avatar]
    documents = params[:user][:documents]

    respond_to do |format|
      if @user.save
        if avatar
          @user.avatar.attach(avatar)
        end
        if documents
          @user.documents.attach(documents)
        end
        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

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    avatar = params[:user][:avatar]
    documents = params[:user][:documents]

    respond_to do |format|
      if @user.update(user_params)
        if avatar
          @user.avatar.attach(avatar)
        end
        if documents
          @user.documents.attach(documents)
        end
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

10. Restart your application. You should now be able to add avatars and documents to users, and retrieve them via the show view.

Active Storage with Amazon Web Services Storage

Moving along, here's how to move your Active Storage files over to Amazon S3:

1. Create a new branch of code, starting from the local branch so that you already have the basics:

  git checkout -b aws

2. Sign in to your AWS account and go to your S3 management console (the URL will be something like https://console.aws.amazon.com/s3/home?region=us-west-2# depending on your region).

3. Create a new bucket (for this tutorial, I'll use gstroop-production). Grant public read access to the bucket.

4. Retrieve the Access Key ID and Secret Access Key for your AWS account. Better yet, create a new pair just for this application.

5. Update your config/storage.yml file with a configuration stanza for Amazon:

amazon:
  service: S3
  access_key_id: ****************
  secret_access_key: *********************************************
  region: us-west-2
  bucket: gstroop-production

NOTE: Obviously, you need to keep those keys confidential. Don't check them into a public repository, for example. You can use the Rails secrets file to store them, or whatever other mechanism you prefer for production secrets.

6. Update your development.rb file to use the Amazon storage:

config.active_storage.service = :amazon

7. Add the AWS SDK gem to your Gemfile:

gem 'aws-sdk'

8. Install the gem:

bundle install

9. Restart the application. You should now be able to add user avatars and documents, and have then stored in the S3 bucket that you configured.

Active Storage with Google Cloud Platform Storage

You can also store your Active Storage files on Google Cloud Platform:

1. Create a new branch of code, starting from the local branch so that you already have the basics:

  git checkout -b gcs

2. Sign in to your Google Cloud Platform account and go to your console (the URL will be something like https://console.cloud.google.com/home/dashboard).

3. You need to create a new project, and then a storage bucket inside of the project. Record the names for both so you can add them to your storage.yml file.

4. You'll need to use GCS's API Manager to create credentials for your new bucket. Create a set of credentials that use the Google Cloud Datastore API. Download your credentials as JSON and store a copy of the file in your project at config/gcs.json.

NOTE: Obviously, you need to keep the keys in this file confidential. Don't check them into a public repository, for example. Manage it the same way you manage your database.yml or other files containing confidential information.

5. Update your storage.yml file with a configuration stanza for Google:

google:
  service: GCS
  project: **********-******
  keyfile: <%= Rails.root.join("config/gcs.json") %>
  bucket: ****-*******-****

6. Update your development.rb file to use the Google storage:

config.active_storage.service = :google

7. Add the Google Cloud Storage gem to your Gemfile:

gem 'google-cloud-storage'

8. Install the gem:

bundle install

9. Restart the application. You should now be able to add user avatars and documents, and have then stored in the Google bucket that you configured.

Active Storage with Microsoft Azure

You can also store your Active Storage files on Microsoft Azure:

1. Create a new branch of code, starting from the local branch so that you already have the basics:

  git checkout -b azure

2. Sign in to your Microsoft Azure account and go to your dashboard (https://portal.azure.com).

3. You need to create a new storage account, and then a container inside of the project. Record the names for both so you can add them to your storage.yml file.

4. You'll also need to navigate to "Access Keys" in the Azure portal and record one of your storage account access keys.

NOTE: Obviously, you need to keep the keys in this file confidential. Don't check them into a public repository, for example. Manage it the same way you manage your database.yml or other files containing confidential information.

5. Update your storage.yml file with a configuration stanza for Azure:

microsoft:
  service: AzureStorage
  # Note that the path must NOT have a trailing slash
  path: https://**********.blob.core.windows.net
  storage_account_name: **********
  storage_access_key: ******************************************
  container: *************

NOTE: Obviously, you need to keep those keys confidential. Don't check them into a public repository, for example. You can use the Rails secrets file to store them, or whatever other mechanism you prefer for production secrets.

6. Update your development.rb file to use the Microsoft Azure storage:

config.active_storage.service = :microsoft

7. Add the Azure gem to your Gemfile:

gem 'azure-core'

8. Install the gem:

bundle install

9. Restart the application. You should now be able to add user avatars and documents, and have then stored in the Azure container that you configured.

NOTE: There is currently a bug preventing attached files being properly retrieved from Azure. See https://github.com/rails/rails/pull/30135

Active Storage Mirroring

1. Create a new branch of code, starting from the local branch so that you already have the basics:

  git checkout -b mirror

2. Create amazon and google stanzas in your config/storage.yml file, following the instructions given above.

3. Update your storage.yml file with a configuration stanza for mirroring:

mirror:
  service: Mirror
  primary: local
  mirrors: [ amazon, google ]

4. Update your development.rb file to use the mirrored storage:<

config.active_storage.service = :mirror

5. Add both the AWS and Google Cloud Storage gems to your Gemfile:

gem 'aws'
gem 'google-cloud-storage'

6. Install the gems:

bundle install

7. Restart the application. You should now be able to add user avatars and documents.

What does mirroring do? It gives you built-in redundancy against cloud service failures. In the case of the configuration above:

  • Files are uploaded to Amazon and Google, and stored locally.
  • Files are served from local storage.

Should AWS go down for an entire region, you'd only need to change the mirror configuration in your config file, restart your server, and you'd be up and running again.

Active Storage Variants

Active Storage also includes built-in support for applying arbitrary transforms to images with MiniMagick. To force all uploaded avatars to 128x128 pixels, follow these steps:

1. Create a new branch of code, starting from the local branch so that you already have the basics:

  git checkout -b variant

2. Update app/views/users/show.html.erb:

<p>
  <%= image_tag(url_for(@user.avatar.variant(resize: "128x128"))) %>
<p>

3. Restart the application. Regardless of the size of avatar you upload, it should be displayed at 128x128 pixels.

Active Storage uses a lazy strategy to create variants. No variants are created at upload time. Rather, the first time you try to access a variant it is created and stored, and then the URL gets it from the new storage location.

Direct Uploads

If you're using cloud storage, Active Storage includes a JavaScript library that can bypass your server entirely, uploading files directly from the browser to the cloud. To use direct upload with AWS, follow these steps:

1. Create a new branch of code, starting from the aws branch so that you already have the basics, including a working AWS configuration:

  git checkout -b direct_upload

2. Add the Active Storage JavaScript to your app/assets/javascripts/application.js file:

//= require activestorage

3. Modify app/views/users/_form.html.erb with the `direct_upload` option:

  <div class="field">
    <%= form.label :avatar %>
    <%= form.file_field :avatar, direct_upload: true %>
  </div>

  <div class="field">
    <%= form.label :documents %>
    <%= form.file_field :documents, multiple: true, direct_upload: true %>
  </div>

4. Restart the application. You should be able to add user avatars and documents.

5. Update the CORS configuration on your Amazon S3 bucket to allow incoming requests:

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

NOTE: This is a super-open CORS policy. In a production application, you'll want to lock things down.

NOTE: At the moment this is working for me in Chrome but not in Firefox. There is some sort of request signing issue that I haven't sussed out. Advice welcome.