Working with Files, Rich Text, and Asynchronous Processing in Ruby on Rails

These notes consolidate the complete content of the class into a coherent chapter. They cover file uploads and storage with Active Storage, rich text editing and rendering with Action Text, handling single and multiple attachments, server-side validations for uploads, environment-specific storage configuration, performance considerations (including N+1 query avoidance), and asynchronous processing with Active Job. The goal is to provide a self-contained, pedagogical resource that can be studied without listening to the original recording.

Content creation date: 2026-09-30 08:31:50

1. Why Files and Rich Text Matter in Modern Rails Apps

Modern applications frequently need to manage user-provided media and formatted content. Examples include profile photos, item images in a catalog, and product descriptions with headings, lists, links, and embedded images. Rails provides two first-class frameworks for these needs:

  • Active Storage: Uploading, storing, transforming, and serving files.
  • Action Text: Creating, storing, sanitizing, and rendering rich text content, including embedded attachments.

Together, they deliver a cohesive solution: Active Storage handles the persistence and delivery of files, while Action Text offers a WYSIWYG editor and safe rendering for formatted content.

2. Active Storage: Architecture, Setup, and Usage

Active Storage separates file content from database records. It stores binary data in a storage service (local disk in development, cloud object storage in production) and persists metadata in dedicated tables to keep application tables lightweight and fast.

2.1. How Active Storage Stores Files

  • File storage: Binary files reside in a configured service such as the server’s local disk (development) or cloud services like Amazon S3, Google Cloud Storage, or DigitalOcean Spaces (production).
  • Database records: Two tables created by Active Storage track files:
    • active_storage_blobs: Stores metadata such as filename, content_type, byte_size, and a unique key referencing the stored file.
    • active_storage_attachments: A polymorphic join linking a blob to a specific record (record_type, record_id) under a named attachment (e.g., :photo).

This design avoids storing large BLOBs in application tables, preventing performance penalties (e.g., Item.all pulling megabytes of image data per row).

2.2. Installing and Enabling Active Storage

  • Ensure the image_processing gem is present for image variants:
    # Gemfile
    gem "image_processing", ">= 1.2"
    Then:
    bundle install
  • Install Active Storage migrations:
    bin/rails active_storage:install
  • Migrate the database:
    bin/rails db:migrate

Active Storage manages its own tables; you will not see new columns added to application models.

2.3. Associating Attachments with Models

Declare attachments in models:

# app/models/item.rb
class Item < ApplicationRecord
has_one_attached :photo
end
  • has_one_attached :photo: One-to-one attachment (e.g., a single item photo).
  • has_many_attached :return_photos: One-to-many attachments (e.g., multiple return photos on a loan record).

Checking for presence:

item = Item.find(1)
if item.photo.attached?
puts "This item has a photo."
end

2.4. Building File Upload Forms

Use file_field in a Rails form:

<!-- app/views/items/_form.html.erb -->
<%= form_with(model: @item) do |f| %>
<div class="field">
<%= f.label :photo %>
<%= f.file_field :photo, accept: "image/png,image/jpeg" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

Notes:

  • Rails automatically sets enctype="multipart/form-data" when a file field is present.
  • accept filters file types in the browser UI (a UX hint, not a security control).

2.5. Strong Parameters for Single and Multiple Attachments

Permit attachments in controllers:

  • Single attachment:
    # app/controllers/items_controller.rb
    class ItemsController < ApplicationController
    private
    def item_params
    params.require(:item).permit(:name, :description, :brand, :photo)
    end
    end
  • Multiple attachments (must permit an array):
    # app/controllers/loans_controller.rb
    class LoansController < ApplicationController
    private
    def loan_params
    params.require(:loan).permit(:return_at, return_photos: [])
    end
    end

2.6. Displaying Attachments

  • Single attachment:
    <!-- app/views/items/show.html.erb -->
    <% if @item.photo.attached? %>
    <%= image_tag @item.photo, alt: "Photo of #{@item.name}", class: "item-image" %>
    <% end %>
  • Multiple attachments:
    <!-- app/views/loans/show.html.erb -->
    <% @loan.return_photos.each do |photo| %>
    <%= image_tag photo, alt: "Return photo" %>
    <% end %>

Always check .attached? (for has_one) or iterate safely (for has_many) before rendering.

2.7. Image Variants for Performance

Large originals slow page loads. Use variants to resize:

# app/models/item.rb
class Item < ApplicationRecord
has_one_attached :photo do |attachable|
attachable.variant :thumb, resize_to_limit: [200, 200]
# attachable.variant :medium, resize_to_limit: [500, 500]
end
end

Render a variant:

<!-- app/views/items/index.html.erb -->
<% @items.each do |item| %>
<div>
<h3><%= item.name %></h3>
<% if item.photo.attached? %>
<%= image_tag item.photo.variant(:thumb), alt: "Thumbnail of #{item.name}" %>
<% end %>
</div>
<% end %>

Variants are processed lazily on first request and then cached. For transformations, install dependencies:

  • Ruby gem:
    # Gemfile
    gem "image_processing", ">= 1.2"
  • System library (libvips):
    • macOS (Homebrew): brew install vips
    • Debian/Ubuntu: sudo apt-get install libvips
    • Arch Linux: sudo pacman -S libvips

2.8. Validating Uploads on the Server

Client-side accept is not security. Enforce limits server-side:

  • Single attachment validation:
    # app/models/item_model.rb
    class ItemModel < ApplicationRecord
    has_one_attached :photo
    validate :photo_is_a_small_image
    private
    def photo_is_a_small_image
    return unless photo.attached?
    unless photo.content_type.in?(%w[image/png image/jpeg])
    errors.add(:photo, "must be a PNG or JPG")
    end
    if photo.byte_size > 5.megabytes
    errors.add(:photo, "must be smaller than 5 MB")
    end
    end
    end
  • Multiple attachments validation:
    # app/models/loan.rb
    class Loan < ApplicationRecord
    has_many_attached :return_photos
    validate :return_photos_are_small_images
    private
    def return_photos_are_small_images
    return unless return_photos.attached?
    return_photos.each do |photo|
    unless photo.content_type.in?(%w[image/png image/jpeg])
    errors.add(:return_photos, "must include only PNG or JPG images")
    end
    if photo.byte_size > 5.megabytes
    errors.add(:return_photos, "each image must be smaller than 5 MB")
    end
    end
    end
    end

Active Storage does not validate content types or sizes by default—add these checks to protect resources and costs.

2.9. Seeding Attachments

Attach files during seeding for realistic dev/test data:

# db/seeds.rb
item = ItemModel.create!(name: "Example Item")
item.photo.attach(
io: File.open(Rails.root.join("db", "seeds", "images", "sample.jpg")),
filename: "sample.jpg",
content_type: "image/jpeg"
)

Run:

bin/rails db:seed

2.10. Configuring Storage Services

Define services in config/storage.yml and select per environment.

  • storage.yml:
    # config/storage.yml
    local:
    service: Disk
    root: <%= Rails.root.join("storage") %>
    s3:
    service: S3
    access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %>
    secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %>
    region: <%= ENV["AWS_REGION"] %>
    bucket: <%= ENV["S3_BUCKET"] %>
    spaces:
    service: S3
    endpoint: <%= ENV["SPACES_ENDPOINT"] %>
    access_key_id: <%= ENV["SPACES_ACCESS_KEY_ID"] %>
    secret_access_key: <%= ENV["SPACES_SECRET_ACCESS_KEY"] %>
    region: <%= ENV["SPACES_REGION"] %>
    bucket: <%= ENV["SPACES_BUCKET"] %>
  • Environment selection:
    # config/environments/development.rb
    config.active_storage.service = :local
    # config/environments/test.rb
    config.active_storage.service = :local
    # config/environments/production.rb
    config.active_storage.service = :spaces # or :s3

Why not local disk in production?

  • App servers scale horizontally; local files are not shared across nodes and can be lost on redeploys.
  • Object storage centralizes files, persists across restarts, scales independently, and is cost-effective.

3. Action Text: Rich Text Editing and Secure Rendering

Action Text brings WYSIWYG editing and safe rich content rendering to Rails. It integrates tightly with Active Storage to handle embedded files.

3.1. Conceptual Model

Declare rich text associations:

# app/models/item.rb
class Item < ApplicationRecord
has_rich_text :description
end

Key points:

  • has_rich_text :description creates an association to ActionText::RichText; it does not add a column to items.
  • The associated record stores structured HTML content in body, accessible and renderable in views.

Inspecting in console:

# ruby
item = Item.first
item.description
# => #<ActionText::RichText ... name: "description", body: "<div>...</div>", record_type: "Item", record_id: 1, ...>

3.2. Migrating from Plain Text to Rich Text

If a model already has a plain text column (e.g., description) and you introduce has_rich_text :description, the association method takes precedence. A safe migration approach:

  1. Add a new rich text association with a different name (e.g., :body).
  2. Write a data migration to copy existing plain text into the new rich text field for each record.
  3. Deploy and verify data.
  4. Remove the old column and optionally rename the rich text association to :description.

This phased strategy prevents data loss or method conflicts during the transition.

3.3. Rich Text Editor in Forms

Use the Action Text form helper to render a full editor:

<!-- app/views/items/_form.html.erb -->
<%= form_with(model: item) do |form| %>
<div class="field">
<%= form.label :description %>
<%= form.rich_text_area :description %>
</div>
<% end %>

Editor implementations:

  • Trix has historically been the default editor in Action Text.
  • Lexi is an upcoming, more modern alternative slated to become the default. The helper rich_text_area remains the same; rendered UI and features may differ by Rails version.

3.4. Embedding Files in Rich Text: Active Storage Integration

Action Text relies on Active Storage for embedding images and other files. If Active Storage is not configured, drag-and-drop uploads in the editor will not persist.

When a user embeds an image:

  • Active Storage uploads the file and creates an ActiveStorage::Blob.
  • An ActiveStorage::Attachment links the blob to the rich text record.
  • The Action Text body stores markup referencing the attachment; rendering resolves and displays the embedded image.

Therefore, to support embedded media, configure Active Storage services and dependencies as described in Section 2.

3.5. Security: Sanitization and XSS Protection

Rendering user-supplied HTML is dangerous due to Cross-Site Scripting (XSS). Action Text mitigates this by sanitizing content against an allowlist:

  • Allowed tags: safe formatting elements like p, strong, ul, li, a, img.
  • Disallowed tags: script and dangerous event handler attributes (e.g., onerror) are removed.

Example view rendering:

<!-- app/views/items/show.html.erb -->
<p>
<strong>Description:</strong>
<%= @item.description %>
</p>

Do not use raw with user-provided data:

  • Default ERB escaping is safe for plain text.
  • Action Text renders sanitized rich content safely.
  • raw(...) bypasses escaping and sanitization and should not be used on user content.

4. Performance: Avoiding N+1 Queries with Active Storage and Action Text

Rendering lists of records with attachments and rich text can incur N+1 queries if associations are not preloaded:

  • Without preloading, each item triggers separate queries to fetch attachments/blobs and rich text records.
  • With preloading, Rails loads all needed associations in a minimal number of queries.

4.1. Inefficient Pattern

# app/controllers/item_models_controller.rb
@item_models = ItemModel.order(:name)
<!-- app/views/item_models/index.html.erb -->
<% @item_models.each do |item_model| %>
<%= image_tag item_model.photo if item_model.photo.attached? %> <!-- extra queries -->
<%= item_model.description %> <!-- extra queries -->
<% end %>

4.2. Efficient Preloading

Use helper scopes:

# app/controllers/item_models_controller.rb
@item_models = ItemModel.order(:name)
.with_attached_photo
.with_rich_text_description

This transforms many per-record queries into a handful of batched queries:

  • One for the items
  • One for attachments
  • One for blobs
  • One for rich text records

Performance remains stable as the number of items grows.

5. Active Job: Asynchronous Workloads for Responsiveness

Some tasks are slow or unreliable within the request-response cycle: sending email, generating reports, processing media, or calling third-party APIs. Active Job provides a uniform interface to enqueue such tasks for background execution, improving perceived speed and reliability.

5.1. How Active Job Works

  • You define a job class with a perform method.
  • perform_later enqueues the job to a persistent queue.
  • Separate worker processes pick jobs from the queue and execute them.
  • Active Storage uses this mechanism internally (e.g., to analyze uploads and extract metadata asynchronously).

5.2. Creating and Enqueuing a Job

# app/jobs/return_reminder_job.rb
class ReturnReminderJob < ApplicationJob
queue_as :default
def perform(user)
UserMailer.reminder_email(user).deliver_now
end
end
# Enqueue from controller or model
user = User.find(params[:id])
ReturnReminderJob.perform_later(user)

Common use cases:

  • Email delivery (welcome emails, reminders)
  • Report generation (build CSV/Excel asynchronously and notify users)
  • File processing (image resizing, video transcoding)
  • Third-party API interactions (offload network delays from web requests)

6. Putting It All Together: End-to-End Examples

6.1. Multiple Images on a Loan

  • Model:
    # app/models/loan.rb
    class Loan < ApplicationRecord
    has_many_attached :return_photos
    validate :return_photos_are_small_images
    private
    def return_photos_are_small_images
    return unless return_photos.attached?
    return_photos.each do |photo|
    unless photo.content_type.in?(%w[image/png image/jpeg])
    errors.add(:return_photos, "must include only PNG or JPG images")
    end
    if photo.byte_size > 5.megabytes
    errors.add(:return_photos, "each image must be smaller than 5 MB")
    end
    end
    end
    end
  • Controller:
    # app/controllers/loans_controller.rb
    class LoansController < ApplicationController
    def create
    @loan = Loan.new(loan_params)
    if @loan.save
    redirect_to @loan, notice: "Loan created"
    else
    render :new, status: :unprocessable_entity
    end
    end
    private
    def loan_params
    params.require(:loan).permit(:return_at, return_photos: [])
    end
    end
  • Form:
    <!-- app/views/loans/new.html.erb -->
    <%= form_with model: @loan do |f| %>
    <%= f.label :return_photos %>
    <%= f.file_field :return_photos, multiple: true, accept: "image/png,image/jpeg" %>
    <%= f.submit "Create Loan" %>
    <% end %>
  • View:
    <!-- app/views/loans/show.html.erb -->
    <% @loan.return_photos.each do |photo| %>
    <%= image_tag photo, alt: "Return photo" %>
    <% end %>

6.2. Rich Descriptions with Embedded Images on an Item

  • Model:
    # app/models/item.rb
    class Item < ApplicationRecord
    has_one_attached :photo
    has_rich_text :description
    # Optional: variants
    has_one_attached :photo do |attachable|
    attachable.variant :thumb, resize_to_limit: [200, 200]
    end
    end
  • Controller (index with preloading):
    # app/controllers/items_controller.rb
    class ItemsController < ApplicationController
    def index
    @items = Item.order(:name)
    .with_attached_photo
    .with_rich_text_description
    end
    end
  • Form:
    <!-- app/views/items/_form.html.erb -->
    <%= form_with(model: @item) do |f| %>
    <div class="field">
    <%= f.label :photo %>
    <%= f.file_field :photo, accept: "image/png,image/jpeg" %>
    </div>
    <div class="field">
    <%= f.label :description %>
    <%= f.rich_text_area :description %>
    </div>
    <div class="actions">
    <%= f.submit %>
    </div>
    <% end %>
  • Index view with variant and rich text:
    <!-- app/views/items/index.html.erb -->
    <% @items.each do |item| %>
    <article>
    <h3><%= item.name %></h3>
    <% if item.photo.attached? %>
    <%= image_tag item.photo.variant(:thumb), alt: "Thumbnail of #{item.name}" %>
    <% end %>
    <div class="description">
    <%= item.description %>
    </div>
    </article>
    <% end %>

7. Operational Considerations and Best Practices

  • Enforce both client- and server-side constraints:
    • Client-side accept improves UX only.
    • Server-side validations enforce allowed content types and file size limits.
  • Choose storage per environment:
    • Local Disk in development/test for simplicity.
    • Cloud object storage in production for durability, scalability, and consistency across servers.
  • Monitor storage usage and costs:
    • Set conservative default size limits.
    • Restrict content types to those required by the domain.
  • Performance:
    • Always preload attachments and rich text on collection views using with_attached_ and with_rich_text_ helpers to avoid N+1 queries.
  • Dependencies for image processing:
    • Ensure image_processing gem and libvips system library are installed to support variants efficiently.

8. Concept Definitions and Relationships

  • Active Storage: Rails framework for file uploads. Separates file content from database rows, supports local and cloud storage, and offers transformations via variants.
  • Attachment Associations:
    • has_one_attached: One file per record (e.g., Item.photo).
    • has_many_attached: Multiple files per record (e.g., Loan.return_photos).
  • Variants: Resized/transformed versions of images for performance; processed lazily and cached for subsequent requests.
  • Action Text: Rails framework for rich text editing and rendering. Stores sanitized HTML content and integrates with Active Storage for embedded files.
  • Sanitization: Security process that removes dangerous tags and attributes from user-supplied HTML to prevent XSS.
  • N+1 Queries: Performance anti-pattern where rendering a list triggers per-record queries for associations; solved by eager loading/preloading.
  • Active Job: Background job framework for asynchronous tasks, improving responsiveness and reliability of web requests.