I was surprised how simple it was to implement Active Storage just following the guide from Rails Guides. This is how I added profile images to Speakers Live in 6 easy steps!
This will give us the correct schema we need for Rails to store our uploads.
bin/rails active_storage:install
bin/rails db:migrate
image
to our controller’s whitelisted paramsdef user_params
params.fetch(:user, {}).permit(:email, :image, [...])
end
has_one_attached :image
form.file_field :image
to our User form<div class="w-full md:w-1/2 px-3 mb-6 md:mb-0">
<%= f.label :image, class: "form-label" %>
<%= f.file_field :image, class: "form-field" %>
</div>
If a user doesn’t have an image, then a broken image will be displayed if we just use:
<%= image_tag user.image,
loading: "lazy", alt: user.name, class: "rounded-full" %>
You’ll need to separately handle a default profile image using the attached?
option. On user, we use a method to decide which image to show (this
presentational logic is a perfect candidate for some sort of presenter)
def displayed_image
if image.attached?
image
else
"path/to/default/image.png"
end
end
Doing it this way allows us to keep using image_tag
without having to make a
shared partial and worrying about different styles in different places:
<%= image_tag user.displayed_image,
loading: "lazy", alt: user.name, class: "rounded-full" %>
The Rails Guide doesn’t show how to do this clearly, but we need to add custom validations to our image uploads.
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.
We can prevent other types of uploads by checking the image.content_type
matches a type from an allowed list.
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 less than 30KB. Incredibly small and efficient on page loads.
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
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. Remember: if you’re storing images locally, you will need to add the storage folder to your deploy symlink otherwise they will stay with the release and be lost on your next deploy.
set :linked_dirs, %w{storage}
Image pre-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.