关于 active-storage
面向 Rails 开发的技能,指导配置 Active Storage,并实现上传、附件、图像变体和存储服务。内容涵盖安全处理附件、预加载以避免 N+1 查询、直接上传及测试。
主要功能
- 01配置 Active Storage 和存储服务
- 02声明单个或多个附件
- 03定义并使用命名图像变体
- 04处理缺失附件并移除文件
- 05预加载附件以避免 N+1 查询
- 06测试上传并隔离并行存储
适用场景
- 01为 Rails 模型添加头像或图片上传
- 02配置本地或云端文件存储
- 03调整并展示附件图片
- 04排查附件加载和查询问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill active-storage安装后在 Claude Code 中输入/active-storage确认已加载。
附带文件reference.md
详细介绍
Rails Active Storage 专家
使用 Active Storage 在 Rails 应用中实现文件上传、附件、图像处理和云存储。
理念
核心原则:
- 始终运行安装生成器 — Active Storage 需要迁移文件和配置
- 使用 libvips 而非 ImageMagick — 速度快 10 倍,内存用量仅为 1/10
- 避免 N+1 查询 — 始终使用
with_attached_<name>作用域 - 处理缺失的附件 — 切勿假设附件一定存在
- 生产环境使用代理,开发环境使用重定向 — 代理模式可与 CDN 配合使用
- 使用具名变体 — 在模型中定义变体,而不是在视图中定义
- 多租户 SQLite 应用 — 使用
activerecord-tenanted时,保存后 blob 会返回 404,除非遵循active-storage-multitenant的做法
何时使用此技能
- 为 Rails 模型添加文件上传功能
- 配置云存储(S3、GCS、Azure)
- 实现图像缩放/转换(变体)
- 设置从浏览器直接上传
- 排查附件问题(文件缺失、变体损坏)
- 在存储服务之间迁移
- 测试文件上传
- 在
activerecord-tenanted应用中排查已保存图像的 404 问题(切换到active-storage-multitenant)
指南
步骤 1:验证设置
始终先检查是否已安装 Active Storage:
# Check for Active Storage tables
bin/rails runner "puts ActiveStorage::Blob.table_exists?"
# Check for storage.yml
ls config/storage.yml
# Check for Active Storage migrations
ls db/migrate/*active_storage*
如果尚未安装:
bin/rails active_storage:install
bin/rails db:migrate
这会创建三张表:
active_storage_blobs— 文件元数据(文件名、content_type、byte_size、checksum)active_storage_attachments— 将模型关联到 blob 的多态连接表active_storage_variant_records— 跟踪已生成的变体
验证图像处理是否可用:
# Check for libvips (preferred)
vips --version
# Or ImageMagick (fallback)
convert --version
如有需要,添加到 Gemfile:
gem "image_processing", "~> 1.2" # Required for variants
步骤 2:配置存储服务
config/storage.yml — 定义可用服务:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
为各环境设置启用的服务:
# config/environments/development.rb
config.active_storage.service = :local
# config/environments/test.rb
config.active_storage.service = :test
# config/environments/production.rb
config.active_storage.service = :amazon # or :google, :azure
云服务配置请参阅 reference.md 中的 S3、GCS 和 Azure 配置说明。
步骤 3:在模型上声明附件
单个文件附件:
class User < ApplicationRecord
has_one_attached :avatar
end
多个文件附件:
class Message < ApplicationRecord
has_many_attached :images
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]
attachable.variant :large, resize_to_limit: [800, 800]
end
end
使用特定存储服务:
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
步骤 4:在控制器中处理附件
强参数 — 允许附件参数:
# has_one_attached
def user_params
params.expect(user: [:name, :email, :avatar])
end
# has_many_attached
def message_params
params.expect(message: [:title, :content, images: []])
end
以编程方式添加文件附件:
# From an upload
user.avatar.attach(params[:avatar])
# From an IO object
user.avatar.attach(
io: File.open("/path/to/file.jpg"),
filename: "avatar.jpg",
content_type: "image/jpeg"
)
# From a URL (download first)
user.avatar.attach(
io: URI.open("https://example.com/photo.jpg"),
filename: "photo.jpg"
)
步骤 5:在视图中显示附件
渲染前始终检查附件是否已添加:
<%# WRONG — will raise if no avatar %>
<%= image_tag user.avatar %>
<%# CORRECT — guard against missing attachment %>
<%= image_tag user.avatar if user.avatar.attached? %>
<%# BEST — use variant with guard %>
<% if user.avatar.attached? %>
<%= image_tag user.avatar.variant(:thumb) %>
<% end %>
使用具名变体(不要使用内联转换):
<%# WRONG — variant defined in view %>
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
<%# CORRECT — named variant from model %>
<%= image_tag user.avatar.variant(:thumb) %>
步骤 6:避免 N+1 查询
这是 Active Storage 最常见的性能错误。
# WRONG — N+1 query for each user's avatar
@users = User.all
# In view: user.avatar triggers a query per user
# CORRECT — eager load attachments
@users = User.with_attached_avatar
# For has_many_attached
@messages = Message.with_attached_images
渲染变体时,也要加载变体记录:
# In view, after eager loading attachments:
message.images.with_all_variant_records.each do |image|
image_tag image.variant(:thumb).processed.url
end
步骤 7:表单设置
简单文件上传:
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar %>
<%= form.submit %>
<% end %>
多个文件:
<%= form_with model: @message do |form| %>
<%= form.file_field :images, multiple: true %>
<%= form.submit %>
<% end %>
更新时保留现有的 has_many_attached 文件:
<%# Without this, updating replaces ALL existing attachments %>
<% @message.images.each do |image| %>
<%= form.hidden_field :images, multiple: true, value: image.signed_id %>
<% end %>
<%= form.file_field :images, multiple: true %>
直接上传(在提交表单前先上传到云端):
<%= form.file_field :avatar, direct_upload: true %>
需要 Active Storage JavaScript。设置说明请参阅 reference.md。
步骤 8:移除附件
# Synchronous — deletes blob + file from storage immediately
user.avatar.purge
# Async — deletes via background job (preferred in controllers)
user.avatar.purge_later
允许用户通过复选框移除附件:
<% if @user.avatar.attached? %>
<%= image_tag @user.avatar.variant(:thumb) %>
<%= form.check_box :remove_avatar, label: "Remove avatar" %>
<% end %>
# In controller or model
after_save :purge_avatar, if: -> { saved_change_to_attribute?(:remove_avatar) }
# Or handle in controller:
def update
@user.update(user_params)
@user.avatar.purge_later if params[:user][:remove_avatar] == "1"
end
步骤 9:测试
在集成测试中使用 file_fixture_upload:
class UsersControllerTest < ActionDispatch::IntegrationTest
test "creates user with avatar" do
post users_path, params: {
user: {
name: "Test",
avatar: file_fixture_upload("avatar.png", "image/png")
}
}
user = User.order(:created_at).last
assert user.avatar.attached?
end
end
将测试文件放在 test/fixtures/files/ 中。
测试后清理上传的文件:
# test/test_helper.rb or application_system_test_case.rb
class ActionDispatch::IntegrationTest
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
end
对于并行测试,为每个进程隔离存储:
class ActiveSupport::TestCase
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
end
基于 fixture 的附件测试请参阅 reference.md。
Agent 常见错误
1. 忘记运行安装生成器
没有迁移文件,Active Storage 将无法工作。始终运行 bin/rails active_storage:install 和 bin/rails db:migrate。
2. 缺少图像处理依赖项
变体需要 gem "image_processing",并且系统上必须安装 libvips 或 ImageMagick。缺少这些依赖时,调用 variant() 会引发错误。
3. 变体处理器语法错误
# WRONG — MiniMagick syntax with Vips processor
variant(resize: "100x100")
# CORRECT — Vips syntax (Rails 7+ default)
variant(resize_to_limit: [100, 100])
4. 未处理缺失的附件
# WILL RAISE if no avatar attached
user.avatar.variant(:thumb)
# SAFE
user.avatar.variant(:thumb) if user.avatar.attached?
5. 附件导致的 N+1 查询
加载集合时,始终使用 with_attached_<name> 作用域。这是最常见的性能问题。
6. 在视图而不是模型中定义变体
在视图中内联定义的变体难以维护,也无法预处理。使用块语法在模型上定义变体。
7. 使用 has_many_attached 时替换而不是追加
默认情况下,向 has_many_attached 添加新文件会替换所有现有附件。使用带有 signed_id 的隐藏字段来保留现有附件。
快速参考
变体转换(Vips — Rails 7+ 默认)
| 转换 | 语法 | 描述 |
|---|---|---|
| 调整大小以适应 | resize_to_limit: [w, h] | 缩小至边界范围内,并保持宽高比 |
| 调整大小以填充 | resize_to_fill: [w, h] | 完全填充边界,裁剪多余部分 |
| 调整大小并留边 | resize_to_fit: [w, h] | 调整至边界范围内,尺寸可能更小 |
| 精确调整大小 | resize_and_pad: [w, h] | 调整至边界范围内,并填充至精确尺寸 |
| 转换格式 | format: :webp | 转换为另一种格式 |
| 质量 | saver: { quality: 80 } | 设置输出质量 |
| 旋转 | rotate: 90 | 按角度旋转 |
关键方法
# Attachment status
record.avatar.attached? # => true/false
record.avatar.blank? # => true/false (opposite of attached?)
# File metadata
record.avatar.filename # => "avatar.png"
record.avatar.content_type # => "image/png"
record.avatar.byte_size # => 12345
record.avatar.blob.metadata # => {"identified"=>true, "width"=>100, "height"=>100, "analyzed"=>true}
# URLs
url_for(record.avatar) # Redirect URL
rails_blob_path(record.avatar) # Path helper
rails_storage_proxy_path(record.avatar) # Proxy URL
# Downloading
record.avatar.download # => binary string
record.avatar.open { |f| ... } # => yields Tempfile
# Purging
record.avatar.purge # Sync delete
record.avatar.purge_later # Async delete
配置选项
# config/application.rb or environment files
config.active_storage.variant_processor = :vips # :vips (default) or :mini_magick
config.active_storage.resolve_model_to_route = :rails_storage_proxy # Enable proxy mode
config.active_storage.draw_routes = false # Disable default routes (for custom auth)
config.active_storage.track_variants = true # Track variant records (default)
config.active_storage.service = :local # Set storage service