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!
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
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_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.
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.
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:
- Access Key (from IAM)
- Secret Access Key (from IAM)
- Bucket name
- 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
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.
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).
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.
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!