Easily Upload Files with Active Storage in Rails 5.2
Many times when developing an application for a customer, the requirement surfaces to handle uploaded files. It is a problem that has been solved many times, but typically not without causing the developer headaches in the process.
When dealing with uploads, one has to consider a multitude of questions: What kinds of file are allowed? How big can the files get? How many can be uploaded and where do you store the files once uploaded? It can be a mess of requirements generated from a simple request.
In the Ruby on Rails world, multiple file uploading tools have been used over the years, like Attachment fu, Paperclip, and Carrierwave. Each had its own way of handling uploads, and none of them were terribly easy to use.
My favorite was Paperclip, as it was the easiest to understand and met most of the requirements typically needed for uploading files. It included enough helper methods to manage uploads and fairly easy ways of shuffling off data to other storage places like Amazon S3. When Rails 5.2 was released in April 2018 one of the major features added was first-class support for uploading file attachments to models in the form of Active Storage. It’s good enough of a solution that Paperclip is now deprecated in favor of the new built-in storage solution.
What is Active Storage?
Simply put, Active Storage is a tool for managing file uploads. Files can be attached to models, transformed using tools such as ImageMagick, and stored in a file on the server or a cloud storage service like Amazon S3, Microsoft Azure Storage, or Google Cloud Storage. It provides an easy-to-use API interface that feels familiar if you already know other Rails tools like ActiveRecord.
Setting up an Application with Active Storage
Let’s start with a new Rails application, using the latest Rails and a reasonable version of Ruby. This assumes you already have Ruby and Ruby on Rails installed.
$ rails new file-upload-demo $ echo "2.5.1" > file-upload-demo/.ruby-version # Use your ruby version here $ echo "file-upload-demo" > file-upload-demo/.ruby-gemset # For both rvm and rbenv $ cd file-upload-demo $ bundle $ rails db:create
Let’s create a simple blog post model, to which we will attach a cover photo to display when the post is shown.
$ rails g scaffold Post title:string body:text
We use the scaffolding here just to simplify creation of the model, controller, view, and routes so we can focus on the stuff specific to Active Storage. Now we can initialize Active Storage:
$ rails active_storage:install $ rails db:migrate
By default, Rails sets up local disk storage for development, and temporary local disk storage for test. If you want to store your files on a cloud service such as Amazon S3, you will have to edit the config to point to your credentials and S3 bucket.
For Amazon S3, we can now take advantage of another new feature of Rails: encrypted secrets. No longer do we have to worry about accidentally exposing our credentials to the world because we can encrypt them in the application to prevent them from being abused or stolen.
$ EDITOR=/usr/bin/vim rails credentials:edit # Replace EDITOR with the path to your favorite
Then uncomment (or add) your Amazon AWS Access ID and Secret Access Key so it looks like the following:
aws: access_key_id: abc123 secret_access_key: def456
Save and quit. Your credentials will be encrypted and unreadable in plain text in the file
config/credentials.enc Next, edit
config/storage.yml and uncomment/add a block for Amazon S3 so it looks like this:
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
Once that is configured, we need to tell Rails which storage system to use for each environment. By default it sets
:local for development,
:test for Test, and
:local for Production. In this case we will want to change this to use Amazon for production, so edit the file
config/environments/production.rb and make the following change:
config.active_storage.service = :amazon
Save and quit with this in place, and we are almost set. The next thing we need is to give Rails the code it needs to be able to upload files to Amazon S3. For that we will need an extra Ruby gem. So add this to your
gem "aws-sdk-s3", require: false
$ bundle install
Using Active Storage
Now that we have the basic setup out of the way, we can start using it in our models, controllers, and views. Since we already set up a scaffold for a blog post, let’s continue with that. We can quickly add the ability to attach uploaded files to our Post model by adding a single line of code in
class Post < ApplicationRecord has_one_attached :cover_photo end
Note something interesting here: We do not need to run
rails db:migrate at this point, because we are not adding any new migrations. There is no new column added to our posts table. All the data will be stored in the existing tables we made when initializing Active Storage.
has_one_attached sets up a one-to-one mapping of a single uploaded file to a single Post instance. If you so desire, you can use
has_many_attached instead to support multiple files uploaded to a model.
Attaching Files to the Model
Now that the model supports a file upload, actually attaching it is pretty easy. In the controller file
app/controllers/posts_controller.rb, the private method
post_params can be modified to let in a new posted parameter for the file we are uploaded.
def post_params params.require(:post).permit(:title, :body, :cover_photo) end
Note that there’s a slight difference if you are accepting multiple files for a parameter with
has_many_attached . In that case you will have to specify an array to be accepted by Rails’ strong parameters. For example, if our model has
has_many_attached :cover_photos , we would do:
def post_params params.require(:post).permit(:title, :body, cover_photos: ) end
Updating the View to Handle File Uploads
The existing Rails form helpers can be used to generate the field needed to attach the file. In the file
file_field helper can be added:
<%= form.label :cover_photo %> <%= form.file_field :cover_photo %>
Then on the view page for the Post, links to both display the file upload and generate a download link can be built using the
rails_blob_path helper methods.
app/views/posts/show.html.erb , add the following:
<%= image_tag url_for(@post.cover_photo) %> <%= link_to @post.cover_photo.filename, rails_blob_path(@post.cover_photo, disposition: "attachment") %>
Once it saves the post, the show page will be displayed and display both the image and provide a link to download the image, complete with the original filename that was uploaded.
Then modify the form in
app/views/posts/_form.html.erb to enable direct uploads:
<%= form.label :cover_photo %> <%= form.file_field :cover_photo, direct_upload: true %>
Now when you upload a file using the scaffold, a progress bar will increase as the file is uploaded, giving your user much-needed feedback that your application is working as intended.
Where to Go from Here
This only scratches the surface of what can be done with Active Storage. Files can be downloaded directly into memory for manipulation (such as image resizing), processed in background/cron jobs, and validations can be placed on file types and sizes. See the Rails Active Storage Guide for more documentation and code samples on how to set that up.
The code created in this article can be downloaded from my (Tim Gourley, software enginner) Github account .
Active Storage takes a lot of pain out of handling file uploads so the business needs of the application can be focused on instead. Give it a try the next time you start up a Rails application and see how easy it is to work with.