AgentSkillsCN

rails-active-storage

Active Storage 文件上传:附件处理、变体生成、校验规则,以及云存储配置

SKILL.md
--- frontmatter
name: rails-active-storage
description: "Active Storage file uploads: attachments, variants, validations, and cloud storage config"

Active Storage

File upload and attachment patterns with Active Storage.


Setup

Single Attachment

ruby
class User < ApplicationRecord
  has_one_attached :avatar, dependent: :purge_later
end

Multiple Attachments

ruby
class Card < ApplicationRecord
  has_many_attached :documents, dependent: :purge_later
end

Dependent options:

  • :purge - Deletes attachment immediately when record is destroyed
  • :purge_later - Queues job to delete attachment (recommended for production)
  • false - Keeps attachment files orphaned (not recommended)

Uploading Files

In Forms

erb
<%= form_with model: @user do |f| %>
  <%= f.file_field :avatar %>
  <%= f.submit %>
<% end %>

Direct Uploads

erb
<%= f.file_field :avatar, direct_upload: true %>

Displaying Images

erb
<% if @user.avatar.attached? %>
  <%= image_tag @user.avatar %>
<% end %>

<%# With variant %>
<%= image_tag @user.avatar.variant(resize_to_limit: [200, 200]) %>

Variants

ruby
# Resize
@user.avatar.variant(resize_to_limit: [300, 300])

# Crop
@user.avatar.variant(resize_to_fill: [300, 300])

# Format
@user.avatar.variant(resize_to_limit: [300, 300], format: :jpg)

Validations

ruby
class User < ApplicationRecord
  has_one_attached :avatar

  validate :avatar_validation

  private
    def avatar_validation
      return unless avatar.attached?

      unless avatar.content_type.in?(%w[image/png image/jpg image/jpeg])
        errors.add(:avatar, "must be a PNG or JPG")
      end

      if avatar.byte_size > 5.megabytes
        errors.add(:avatar, "must be less than 5MB")
      end
    end
end

Downloading Files

Small Files

ruby
# In controller
def download
  send_data @user.avatar.download,
    filename: @user.avatar.filename.to_s,
    type: @user.avatar.content_type,
    disposition: "attachment"
end

Large Files (Streaming)

ruby
# Redirect to cloud storage URL (recommended for large files)
def download
  redirect_to rails_blob_url(@document), allow_other_host: true
end

# Or use X-Sendfile/X-Accel-Redirect
# Configure your web server (nginx/apache) for efficient file serving
send_file @user.avatar.service_url

Removing Attachments

ruby
# Remove single attachment
@user.avatar.purge        # Synchronous
@user.avatar.purge_later  # Background job (recommended)

# Remove all attachments from a collection
@card.documents.purge_all        # Synchronous
@card.documents.purge_later      # Background job (recommended)

# Check if attached
@user.avatar.attached?  # => true/false

Service Configuration

Amazon S3

yaml
# config/storage.yml
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 Cloud Storage

yaml
# config/storage.yml
google:
  service: GCS
  project: my-project
  credentials: <%= Rails.application.credentials.dig(:gcs, :keyfile) %>
  bucket: my-app-<%= Rails.env %>

Microsoft Azure

yaml
# config/storage.yml
microsoft:
  service: AzureStorage
  storage_account_name: <%= Rails.application.credentials.dig(:azure, :storage_account_name) %>
  storage_access_key: <%= Rails.application.credentials.dig(:azure, :storage_access_key) %>
  container: my-app-<%= Rails.env %>

Local (Development/Test)

yaml
# config/storage.yml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# config/environments/development.rb
config.active_storage.service = :local

# config/environments/production.rb
config.active_storage.service = :amazon

Advanced Validations

ruby
class User < ApplicationRecord
  has_one_attached :avatar, dependent: :purge_later
  has_many_attached :documents, dependent: :purge_later

  validate :avatar_validation
  validate :documents_validation

  private
    def avatar_validation
      return unless avatar.attached?

      # Content type validation
      acceptable_types = %w[image/png image/jpg image/jpeg image/gif]
      unless avatar.content_type.in?(acceptable_types)
        errors.add(:avatar, "must be a PNG, JPG, or GIF")
      end

      # File size validation
      if avatar.byte_size > 5.megabytes
        errors.add(:avatar, "must be less than 5MB")
      end

      # Image dimensions (requires image_processing gem)
      image = MiniMagick::Image.read(avatar.download)
      if image.width < 100 || image.height < 100
        errors.add(:avatar, "must be at least 100x100 pixels")
      end
    end

    def documents_validation
      return unless documents.attached?

      # Validate total count
      if documents.count > 10
        errors.add(:documents, "cannot exceed 10 files")
      end

      # Validate each document
      documents.each do |document|
        # File size
        if document.byte_size > 10.megabytes
          errors.add(:documents, "#{document.filename} must be less than 10MB")
        end

        # Content type
        acceptable_types = %w[application/pdf image/png image/jpg]
        unless document.content_type.in?(acceptable_types)
          errors.add(:documents, "#{document.filename} must be a PDF or image")
        end
      end
    end
end

Virus Scanning (Production Pattern)

ruby
# app/models/concerns/virus_scannable.rb
module VirusScannable
  extend ActiveSupport::Concern

  included do
    before_save :scan_attachments
  end

  private
    def scan_attachments
      self.class.attachment_reflections.each_key do |name|
        attachment = public_send(name)
        next unless attachment.attached? && attachment.changed?

        if attachment.is_a?(ActiveStorage::Attached::Many)
          attachment.each { |file| scan_file(file, name) }
        else
          scan_file(attachment, name)
        end
      end
    end

    def scan_file(file, attribute_name)
      # Example using ClamAV or similar service
      scanner = VirusScanner.new(file)
      if scanner.infected?
        errors.add(attribute_name, "contains a virus")
        file.purge
      end
    end
end

# Usage
class User < ApplicationRecord
  include VirusScannable
  has_one_attached :avatar
end

Summary

  • has_one_attached: Single file with dependent: :purge_later
  • has_many_attached: Multiple files with proper validations
  • Variants: Image transformations (resize, crop, format)
  • Direct Upload: Upload to cloud storage directly (faster)
  • Validations: Content type, file size, dimensions, count
  • Service Config: S3, GCS, Azure for production
  • Streaming: Use redirects for large file downloads
  • Virus Scanning: Add security layer for uploads