active-storage
This skill should be used when the user asks about "file uploads", "Active Storage", "attachments", "has_one_attached", "has_many_attached", "image variants", "S3", "cloud storage", "direct uploads", "file processing", "image transformations", or needs guidance on handling file uploads in Rails applications.
When & Why to Use This Skill
This Claude skill provides a comprehensive guide and ready-to-use code snippets for implementing Active Storage in Ruby on Rails applications. It covers the entire lifecycle of file management, including model configuration with 'has_one_attached' and 'has_many_attached', cloud storage integration (AWS S3, GCS, Azure), direct uploads, and advanced image transformations. It is designed to help developers streamline file upload workflows and optimize cloud storage configurations.
Use Cases
- Implementing user profile picture uploads with automatic thumbnail generation and resizing variants.
- Configuring production-ready cloud storage services like Amazon S3 or Google Cloud Storage for Rails applications.
- Setting up direct-to-cloud uploads using JavaScript to improve application performance and reduce server load.
- Managing complex file attachments for document management systems, including PDF previews and multi-file uploads.
- Migrating or mirroring file storage between different cloud providers using the Active Storage Mirror service.
| name | active-storage |
|---|---|
| description | This skill should be used when the user asks about "file uploads", "Active Storage", "attachments", "has_one_attached", "has_many_attached", "image variants", "S3", "cloud storage", "direct uploads", "file processing", "image transformations", or needs guidance on handling file uploads in Rails applications. |
| version | 1.0.0 |
Active Storage
Comprehensive guide to file uploads and cloud storage in Rails.
Setup
rails active_storage:install
rails db:migrate
This creates:
active_storage_blobs- File metadataactive_storage_attachments- Polymorphic join tableactive_storage_variant_records- Cached variant info
Model Configuration
Single Attachment
class User < ApplicationRecord
has_one_attached :avatar
end
Multiple Attachments
class Article < ApplicationRecord
has_many_attached :images
end
With Service Selection
class Document < ApplicationRecord
has_one_attached :file, service: :amazon
end
With Variants
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
Attaching Files
From Form Upload
# Controller
def create
@user = User.new(user_params)
@user.save
end
def user_params
params.require(:user).permit(:name, :avatar, images: [])
end
<%# Form %>
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar %>
<%= form.file_field :images, multiple: true %>
<%= form.submit %>
<% end %>
Programmatically
# From file
user.avatar.attach(io: File.open("/path/to/photo.jpg"), filename: "photo.jpg")
# From uploaded file
user.avatar.attach(params[:avatar])
# From URL (downloaded)
user.avatar.attach(
io: URI.open("https://example.com/photo.jpg"),
filename: "downloaded.jpg",
content_type: "image/jpeg"
)
# From string content
user.avatar.attach(
io: StringIO.new(pdf_content),
filename: "document.pdf",
content_type: "application/pdf"
)
Check Attachment Status
user.avatar.attached? # true/false
user.images.attached? # true/false
user.images.any? # true/false
user.images.count # number of attachments
Displaying Files
URLs
<%# Direct URL %>
<%= url_for(@user.avatar) %>
<%# Download URL %>
<%= rails_blob_path(@user.avatar, disposition: "attachment") %>
<%# Image tag %>
<%= image_tag @user.avatar %>
<%# With fallback %>
<%= image_tag(@user.avatar.attached? ? @user.avatar : "default_avatar.png") %>
Variants (Image Transformations)
<%# On-the-fly transformation %>
<%= image_tag @user.avatar.variant(resize_to_limit: [100, 100]) %>
<%# Named variant %>
<%= image_tag @user.avatar.variant(:thumb) %>
<%# Complex transformations %>
<%= image_tag @user.avatar.variant(
resize_to_fill: [200, 200],
format: :webp,
saver: { quality: 80 }
) %>
Common Transformations
| Method | Description |
|---|---|
resize_to_limit: [w, h] |
Resize to fit within bounds |
resize_to_fill: [w, h] |
Resize and crop to fill |
resize_to_fit: [w, h] |
Resize to fit exactly |
resize_and_pad: [w, h] |
Resize and pad to fill |
crop: "100x100+10+10" |
Crop specific area |
rotate: 90 |
Rotate degrees |
format: :webp |
Convert format |
Preview (PDFs, Videos)
<%# PDF preview (first page) %>
<%= image_tag @document.file.preview(resize_to_limit: [200, 200]) %>
<%# Video preview (frame) %>
<%= image_tag @article.video.preview(resize_to_limit: [300, 200]) %>
Direct Uploads
Setup
<%# Include JS %>
<%= javascript_include_tag "activestorage" %>
<%# Form with direct upload %>
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar, direct_upload: true %>
<% end %>
JavaScript Events
// Listen for upload events
addEventListener("direct-upload:initialize", event => {
const { target, detail } = event
const { id, file } = detail
// Show upload UI
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail
// Update progress bar
})
addEventListener("direct-upload:error", event => {
const { id, error } = event.detail
// Handle error
})
addEventListener("direct-uploads:end", event => {
// All uploads complete
})
Storage Services
Configuration
# config/storage.yml
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: us-east-1
bucket: my-app-<%= Rails.env %>
google:
service: GCS
project: my-project
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
bucket: my-app-<%= Rails.env %>
azure:
service: AzureStorage
storage_account_name: myaccount
storage_access_key: <%= Rails.application.credentials.dig(:azure, :storage_access_key) %>
container: my-container
mirror:
service: Mirror
primary: amazon
mirrors: [ google, azure ]
Environment Selection
# config/environments/development.rb
config.active_storage.service = :local
# config/environments/production.rb
config.active_storage.service = :amazon
Public Files
# config/storage.yml
amazon_public:
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: my-public-bucket
public: true
class Article < ApplicationRecord
has_many_attached :images, service: :amazon_public
end
Validations
Using active_storage_validations Gem
# Gemfile
gem "active_storage_validations"
# Model
class User < ApplicationRecord
has_one_attached :avatar
validates :avatar,
attached: true,
content_type: ["image/png", "image/jpeg"],
size: { less_than: 5.megabytes }
end
class Article < ApplicationRecord
has_many_attached :images
validates :images,
content_type: /\Aimage\/.*\z/,
size: { less_than: 10.megabytes },
limit: { max: 10 }
end
Custom 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.megabyte
errors.add(:avatar, "is too large (max 5MB)")
end
acceptable_types = ["image/jpeg", "image/png", "image/gif"]
unless acceptable_types.include?(avatar.content_type)
errors.add(:avatar, "must be JPEG, PNG, or GIF")
end
end
end
Downloading and Processing
# Download content
binary = user.avatar.download
# Open as tempfile
user.avatar.open do |file|
# file is a Tempfile
ImageProcessor.process(file.path)
end
# Access metadata
user.avatar.blob.byte_size
user.avatar.blob.content_type
user.avatar.blob.filename
user.avatar.blob.created_at
Removing Attachments
# Remove single attachment
user.avatar.purge # Sync
user.avatar.purge_later # Async (recommended)
# Remove specific from many
user.images.find(attachment_id).purge_later
# Remove all
user.images.purge_later
Eager Loading
# Avoid N+1 queries
@users = User.with_attached_avatar
@articles = Article.with_attached_images
# Named scope automatically created
User.with_attached_avatar.where(active: true)
Testing
# test/models/user_test.rb
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "can attach avatar" do
user = users(:one)
user.avatar.attach(
io: file_fixture("avatar.png").open,
filename: "avatar.png",
content_type: "image/png"
)
assert user.avatar.attached?
end
end