| name | active-storage-setup |
| description | Configures Active Storage for file uploads with variants and direct uploads. Use when adding file uploads, image attachments, document storage, generating thumbnails, or when user mentions Active Storage, file upload, attachments, or image processing. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
Active Storage Setup for Rails 8
Overview
Active Storage handles file uploads in Rails:
- Cloud storage (S3, GCS, Azure) or local disk
- Image variants (thumbnails, resizing)
- Direct uploads from browser
- Polymorphic attachments
Quick Start
bin/rails active_storage:install
bin/rails db:migrate
bundle add image_processing
Configuration
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
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: <%= Rails.application.credentials.dig(:aws, :bucket) %>
config.active_storage.service = :local
config.active_storage.service = :amazon
Model Attachments
Single Attachment
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
attachable.variant :medium, resize_to_limit: [300, 300]
end
end
Multiple Attachments
class Event < ApplicationRecord
has_many_attached :photos
has_many_attached :documents
end
Validations
Manual Validation
class User < ApplicationRecord
has_one_attached :avatar
validate :acceptable_avatar
private
def acceptable_avatar
return unless avatar.attached?
unless avatar.blob.byte_size <= 5.megabytes
errors.add(:avatar, "is too large (max 5MB)")
end
acceptable_types = ["image/jpeg", "image/png", "image/webp"]
unless acceptable_types.include?(avatar.content_type)
errors.add(:avatar, "must be a JPEG, PNG, or WebP")
end
end
end
With active_storage_validations Gem
gem "active_storage_validations"
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar,
content_type: ["image/png", "image/jpeg", "image/webp"],
size: { less_than: 5.megabytes }
end
Image Variants
resize_to_limit: [300, 300]
resize_to_fill: [300, 300]
resize_to_limit: [300, 300], format: :webp, saver: { quality: 80 }
Using in Views
<% if user.avatar.attached? %>
<%= image_tag user.avatar.variant(:thumb), alt: user.name %>
<% else %>
<%= image_tag "default-avatar.png", alt: "Default" %>
<% end %>
Testing Attachments (Minitest)
Model Test
require "test_helper"
class UserAttachmentTest < ActiveSupport::TestCase
setup do
@user = users(:one)
end
test "attaches an avatar" do
@user.avatar.attach(
io: File.open(Rails.root.join("test/fixtures/files/avatar.jpg")),
filename: "avatar.jpg",
content_type: "image/jpeg"
)
assert @user.avatar.attached?
end
test "rejects oversized avatar" do
@user.avatar.attach(
io: StringIO.new("x" * 6.megabytes),
filename: "large.jpg",
content_type: "image/jpeg"
)
assert_not @user.valid?
assert_includes @user.errors[:avatar], "is too large (max 5MB)"
end
test "rejects invalid content type" do
@user.avatar.attach(
io: File.open(Rails.root.join("test/fixtures/files/document.pdf")),
filename: "doc.pdf",
content_type: "application/pdf"
)
assert_not @user.valid?
assert @user.errors[:avatar].any?
end
end
Controller Test
require "test_helper"
class UsersUploadTest < ActionDispatch::IntegrationTest
setup do
@user = users(:one)
sign_in @user
end
test "uploads avatar" do
avatar = fixture_file_upload("avatar.jpg", "image/jpeg")
patch user_path(@user), params: { user: { avatar: avatar } }
assert @user.reload.avatar.attached?
end
test "removes avatar" do
@user.avatar.attach(
io: File.open(Rails.root.join("test/fixtures/files/avatar.jpg")),
filename: "avatar.jpg",
content_type: "image/jpeg"
)
delete remove_avatar_user_path(@user)
assert_not @user.reload.avatar.attached?
end
end
Fixtures Setup
Place test files in test/fixtures/files/:
test/fixtures/files/
โโโ avatar.jpg
โโโ document.pdf
โโโ photo.png
Controller Handling
class UsersController < ApplicationController
def update
if @user.update(user_params)
redirect_to @user, notice: "Profile updated"
else
render :edit, status: :unprocessable_entity
end
end
def remove_avatar
@user.avatar.purge
redirect_to edit_user_path(@user), notice: "Avatar removed"
end
private
def user_params
params.require(:user).permit(:name, :email, :avatar)
end
end
Multiple Uploads
def event_params
params.require(:event).permit(:name, photos: [], documents: [])
end
Forms
<%= form_with model: @user do |f| %>
<div>
<%= f.label :avatar %>
<%= f.file_field :avatar, accept: "image/png,image/jpeg,image/webp" %>
<% if @user.avatar.attached? %>
<%= image_tag @user.avatar.variant(:thumb), class: "rounded mt-2" %>
<% end %>
</div>
<%= f.submit %>
<% end %>
Direct Uploads
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
<%= f.file_field :photos, multiple: true, direct_upload: true %>
Performance Tips
User.with_attached_avatar.limit(10)
Event.with_attached_photos.with_attached_documents
Checklist