User Profile Images with Active Storage

Published on May 11, 2020

I was surprised how simple it was to actually implement.

I followed the Active Storage Overview guide from Ruby guides: https://edgeguides.rubyonrails.org/active_storage_overview.html

I was adding profile images to Speakers Live: https://speakerslive.tech

  1. Run the task to generate the migrations: `bin/rails active_storage:install`
  2. Run the migrations: `bin/rails db:migrate`

This will give us the correct schema we need for Rails to store our uploads. So, let's update the model and controllers to get that working.

3. Add `image` to our controller's whitelisted params

4. Update the model with: `has_one_attached :image`

5. Add `form.file_field :image` to our User form

6. Update the display to use the dynamic images:

<% if user.image.attached? %>
  <%= image_tag user.image, loading: "lazy", alt: user.name, class: "rounded-full" %>
<% else %>
  <%= image_tag "profile-default.png", loading: "lazy", alt: "No profile image (default)", class: "rounded-full" %>
<% end %>

(I moved this into a partial, so that we could reuse this in multiple places, however, this can be improved by not using view logic. In fact, we should probably just update the image path, so we can easily update classes on different pages)

Lastly, we're only dealing with images, but Active Storage allows uploads of all file times - anything you can find on your computer; PDF, EXE, MP4.

The Ruby guide doesn't show how to do this clearly, but we need to add custom validations to prevent other types of uploads.

7. Add a custom validations to our upload model to allow our desired file types

validate :acceptable_image_type?

def acceptable_image_type?
  return unless image.attached?
  return if image.content_type.in? ["image/png", "image/jpeg"]
  errors.add :image, "must be a PNG or JPG"
end

Lastly, we don't want them to upload HUGE images. Twitter stores your profile image at < 30KB. Incredibly small and efficient on page loads.

8. Add a custom validations to our upload model to limit uploaded file sizes

validate :acceptable_image_size?

def acceptable_image_size?
  return unless image.attached?
  return unless image.byte_size > 1.megabyte
  errors.add :image, "is over 1MB"
end

That's it. Now you have image uploads in a Rails application.

Some more improvements could be made though:

  • In our custom validations, we're checking if the file is attached multiple times (once in each validation).
  • This implementation is using the default local storage - on your server. The guide shows examples of setting up externally with AWS and Google Cloud.
  • Image processing - for our application, we know we never want profile images bigger than 150x150. Rather than storing the full image, we could process those images when they are upload to be our desired size. This will improve load times, storage space and allow us to handle initially larger images.