From ruby-on-rails
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.
npx claudepluginhub bastos/ruby-plugin-marketplace --plugin ruby-on-railsThis skill uses the workspace's default tool permissions.
Comprehensive guide to file uploads and cloud storage in Rails.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Comprehensive guide to file uploads and cloud storage in Rails.
rails active_storage:install
rails db:migrate
This creates:
active_storage_blobs - File metadataactive_storage_attachments - Polymorphic join tableactive_storage_variant_records - Cached variant infoclass User < ApplicationRecord
has_one_attached :avatar
end
class Article < ApplicationRecord
has_many_attached :images
end
class Document < ApplicationRecord
has_one_attached :file, service: :amazon
end
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
# 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 %>
# 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"
)
user.avatar.attached? # true/false
user.images.attached? # true/false
user.images.any? # true/false
user.images.count # number of attachments
<%# 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") %>
<%# 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 }
) %>
| 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 |
<%# 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]) %>
<%# Include JS %>
<%= javascript_include_tag "activestorage" %>
<%# Form with direct upload %>
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar, direct_upload: true %>
<% end %>
// 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
})
# 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 ]
# config/environments/development.rb
config.active_storage.service = :local
# config/environments/production.rb
config.active_storage.service = :amazon
# 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
# 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
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
# 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
# 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
# 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)
# 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