mcpField
A
active-storage

active-storage

github.com/thinkoodle开发7176 次安装8 周前更新
代码仓库

关于 active-storage

面向 Rails 开发的技能,指导配置 Active Storage,并实现上传、附件、图像变体和存储服务。内容涵盖安全处理附件、预加载以避免 N+1 查询、直接上传及测试。

主要功能

  1. 01配置 Active Storage 和存储服务
  2. 02声明单个或多个附件
  3. 03定义并使用命名图像变体
  4. 04处理缺失附件并移除文件
  5. 05预加载附件以避免 N+1 查询
  6. 06测试上传并隔离并行存储

适用场景

  1. 01为 Rails 模型添加头像或图片上传
  2. 02配置本地或云端文件存储
  3. 03调整并展示附件图片
  4. 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 应用中实现文件上传、附件、图像处理和云存储。

理念

核心原则:

  1. 始终运行安装生成器 — Active Storage 需要迁移文件和配置
  2. 使用 libvips 而非 ImageMagick — 速度快 10 倍,内存用量仅为 1/10
  3. 避免 N+1 查询 — 始终使用 with_attached_<name> 作用域
  4. 处理缺失的附件 — 切勿假设附件一定存在
  5. 生产环境使用代理,开发环境使用重定向 — 代理模式可与 CDN 配合使用
  6. 使用具名变体 — 在模型中定义变体,而不是在视图中定义
  7. 多租户 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

评论

支持 Markdown