My site has been down for awhile due to an incident on service providers end. My server was gone for good after storage system crashed and all backups were on same storage cluster, so no help from there either.

It was a lost but also a new opportunity to start my site from scratch. I moved back to Digital Ocean (click the link to get $25 free credits to Digital Ocean!) which has served me well for years. It was a mistake to jump out of that train back then.

That's that, I suppose you are here for Rails and ActiveStorage so let's get our hands dirty!

ActiveStorage?

Rails 5.2 introduced a lot of fun stuff and one important of many things is ActiveStorage. For ages there have been different gems to ease file uploading and handling, now there is ActiveStorage which does almost everything same without any external dependencies. Almost everything is done automagically, even image resizing and that's quite something, isn't it!?

ActiveStorage is easy to use, so you don't need to have much experience with Rails to utilize it on your projects.

This article will only go thru the basics. I don't cover up everything, only how to set up your AWS S3 and we also create a small project to upload an image. Upcoming articles will cover up how to manipulate your files, use different storages and so on. Think this as a part of a mini course.

First things first

You should already have the latest Rails installed and working in your development environment. If you don't, bookmark this page now, set up your environment and head back here.

Let's create a new project

rails new storage-tutorial

For sake of simplicity, we create a very basic example which only has one page for everything. Rails has nice scaffold feature for easy creation of all this but I am quite an old school guy and like to do things manually. Learning by doing is also something that you don't get with automatic generators.

Since we want to keep this example as small as possible we create only things what we really need.

Start form migration file, run rails g migration CreateImages and you will get migration file. Modify your migration file to look like:

class CreateImages < ActiveRecord::Migration[5.2]
  def change
    create_table :images do |t|
      t.string :title
      t.timestamps
    end
  end
end

We only need a title for our image and as a good habit, I like to add timestamps for records. Remember to run rails db:migrate after you have edited your migration.

Then, create model app/models/image.rb with content

class Image < ApplicationRecord
end

Add images as a resource to your config/routes.rb

Rails.application.routes.draw do
  resources :images
end

If everything is good, rails routes should give you

Prefix          Verb   URI Pattern                Controller#Action
images          GET    /images(.:format)          images#index
                POST   /images(.:format)          images#create
new_image       GET    /images/new(.:format)      images#new
edit_image      GET    /images/:id/edit(.:format) images#edit
image           GET    /images/:id(.:format)      images#show
                PATCH  /images/:id(.:format)      images#update
                PUT    /images/:id(.:format)      images#update
                DELETE /images/:id(.:format)      images#destroy

Now you have model and route as they should be. Next, you need a controller and view. Let's start with a controller. Create app/controllers/image_controller.rb with content:

class ImagesController < ApplicationController

  def index
    @image = Image.new
    @images = Image.all
  end

  def create
    Image.create(upload_params)
    redirect_to images_path
  end

  private

  def upload_params
    params.require(:image).permit(:title)
  end
end

In real life, we would also need delete and other methods. For now, we just want to handle upload and view images. Let's keep our controller in this state for now.

Create a folder and view app/views/images/index.html.erb. Since Rails 5.1 there is form_with helper available, it unifies form_tag and form_for to one. I go with a new helper (form_with) and our view should be like:

<h3>Upload image</h3>

<%= form_with model: @image do |f| %>

    <%= f.label :title %><br>
    <%= f.text_field :title %><br>
    
    <%= f.submit %>
    
<% end %>

<hr>

<% @images.each do |i| %>
  <strong><%= i.title %></b><br>
<% end %>

Now we have a basic setup for creating image records, without actual image file,  and our software also lists all images on the same page below form. Next we need to setup Amazon S3 and IAM as well as ActiveStorage.

Amazon S3 and IAM

Head now to https://aws.amazon.com and login into Amazon AWS or create a new account.

First: IAM

Once you are in, find your way to IAM-panel. Open up users from the menu and Add user.

Give a name for your user, like "storage" and select Programmatic Access for the access type.

Next, you need to add permissions for your user. In real life, you may want to create a group and define permissions more properly, this time you can select "Attach existing policies directly"-tab and search proper permission with keyword S3. You should see AmazonS3FullAccess on the list, select that. When you are creating real-world software you really should consider how much permission your software has over your S3 or other AWS services.

Review your settings and create user.

Now you can see user listed on screen with Access Key ID and Secret Access Key. Both of these are very important, copy-paste credential now to a safe place. You can also download all fields in CSV format, that would be a good way to go also.

Then: S3

Open up S3 service next. Click Create Bucket.

Choose a name for your bucket, something simple is good again.

Select region for your bucket. Region affects S3 pricing but also plays important role in your application when you need to consider where to store your data. If your software is for people inside EU then I would recommend using the EU area. I use Ireland in this example.

Click next until bucket is created.

There is now a couple important things:

  1. Access Key (from IAM)
  2. Secret Access Key (from IAM)
  3. Bucket name
  4. Bucket region

We selected Ireland as a region but we just can't yell out "IRELAND!" when region information is needed. There is a specific region name for each region and country and in this case, it's eu-west-1. If you selected something else, see AWS Regions and Endpoints.

Tadaa! Amazon is set up!

ActiveStorage setup and uploading

Start with adding gem "aws-sdk-s3" to your Gemfile. Remember to run bundle install.

Then, ActiveStorage migrations:

rails active_storage:install

rails db:migrate

In the first line we ask Rails to create a migration for ActiveStorage, this will generate two files. Then we run the migration.

Next, we need to set up credentials. Rails 5.1 introduced encrypted secrets (secrets.yml and secrets.yml.enc), this is a bit confusing since there is encrypted ones and other ones without encryption. At least I think this is confusing and probably that was the case overally since Rails 5.2 replaces this with encrypted credentials, which is a much better way to do it.

Credentials are saved in config/credentials.yml.enc in Rails 5.2, you shouldn't edit the file directly since it is - encrypted. To add/edit credentials you need to run rails credentials:edit. If you have already defined editor for your environment it will be fired up. If you haven't you will get error:

No $EDITOR to open file in. Assign one like this:

EDITOR="mate --wait" bin/rails credentials:edit

For editors that fork and exit immediately, it's important to pass a wait flag,
otherwise the credentials will be saved immediately with no chance to edit.

I like pico so I would run this like:

EDITOR=/usr/bin/pico rails credentials:edit

Once your editor is open, you should see these lines on top:

# aws:
#   access_key_id: 123
#   secret_access_key: 345

Uncomment these lines and add your credentials you got from IAM. Save your credentials and quit the editor. Rails will now encrypt your credential file.

Next, you need to set up config/storage.yml. Following code should be already in your file and you just need to uncomment it. There is also examples for Google and Microsoft which we are not going thru now.

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: eu-west-1
  bucket: your-bucket-name

Notice: the region is eu-west-1, it may be something else if you select other than Ireland.

Next important thing is; Rails uses :local storage for development as well as :local defaults to a production environment. This means files are saved locally, not to Amazon S3. To test our code you need to fix that for development environment (and eventually to production also).

Open up config/environments/development.rb and add following line:

config.active_storage.service = :amazon

That's that! Now we just need to make use of all this.

Upload and view images

To make our Image-model aware of images we need to link files with has_one_attached. Your model should look like:

class Image < ApplicationRecord

    has_one_attached :uploaded_image

end

Your controller needs to do some work with uploads as well. Modify it a bit and add :uploaded_image to end of permitted parameters. Like this:

class ImagesController < ApplicationController

  def index
    @image = Image.new
    @images = Image.all
  end

  def create
    Image.create(upload_params)
    redirect_to images_path
  end

  private

  def upload_params
    params.require(:image).permit(:title, :uploaded_image)
  end
end

We shouldn't forget our view, it needs some love too. Add file upload there as well as way to view files which are already uploaded.

<h3>Upload image</h3>
<%= form_with model: @image do |f| %>
    <%= f.label :title %><br>
    <%= f.text_field :title %><br>
    <%= f.label :uploaded_image %>
    <%= f.file_field :uploaded_image %>
    <%= f.submit %>
<% end %>

<hr>

<% @images.each do |i| %>
  <strong><%= i.title %></b><br>
  <% if i.uploaded_image %>
    <%= image_tag url_for(i.uploaded_image) %><br>
  <% end %>
<% end %>

:uploaded_image is added to form with file_field, this handles file delivery to a controller. And on the bottom we have a way to load and view each image.

Final words

Now you are able to upload files to Amazon S3. This example doesn't do any fancy stuff, it won't even delete your files. The upcoming article will dive deeper on these topics, be ready!