Attachments and cloud storage (S3-compatible)
Uploading images and files looks trivial until you build it, and almost every app needs it: user avatars are one of the most common features there is. RailsFast ships it working. As long as your R2 credentials are set up (see Configuration), there is nothing else to do.
Avatars, the worked example
Every user in the demo app can upload and change an avatar. The attachment and its sizes are declared on the User model:
# app/models/user.rb
has_one_attached :avatar do |attachable|
attachable.variant :thumb,
resize_to_limit: [ 100, 100 ],
format: :webp,
saver: { strip: true, quality: 80 }
# ... plus :small (200x200) and :medium (400x400)
end
Each variant is converted to WebP and stripped of metadata, so pages load fast and photos do not leak EXIF location data.
Uploads are validated in the model, so every path that can attach a photo (Settings today, an onboarding step or an API tomorrow) gets the same rules:
- Type: JPEG, PNG, GIF or WebP (
User::AVATAR_CONTENT_TYPES). The check reads the content type Active Storage detected from the bytes, not what the browser claimed. - Size: 5 MB at most (
User::AVATAR_MAX_BYTES). - It must open as an image: the bytes are decoded with the same libvips loaders the variants use, so a file that only looks like a photo by its name is refused with a message instead of breaking every page that shows it.
Before you call avatar.variant(...) in a view, ask user.avatar_renderable? rather than avatar.attached?. It returns false for a stored file Active Storage cannot transform and for an upload the model just refused, and the avatar partial falls back to initials in both cases.
To add attachments to any other model, copy what the avatar does and read the official Active Storage guide.
Image processing: image_processing 2 and ruby-vips
Variants are generated by image_processing 2.2 on top of libvips. Since version 2, image_processing no longer pulls in ruby-vips, so the Gemfile declares it right below:
gem "image_processing", "~> 2.2"
gem "ruby-vips", "~> 2.3", require: false
Keep that line exactly as it is, require: false included. Active Storage loads the gem itself when libvips is installed, and only logs a warning on a machine without it. If a template merge drops the line, the app still boots, but every avatar upload fails with a 500, every variant fails, and new blobs get no width or height. After updating, check it with:
bundle exec ruby -e 'require "vips"'
Production always has libvips: the Dockerfile installs it.
image_processing 2 no longer sharpens after a libvips resize, so thumbnails come out slightly softer than on 1.x. If you prefer the old look, pass the option on the variant: resize_to_limit: [ 100, 100, { sharpen: true } ].
Moving to image_processing 2 also closes remote code execution holes in 1.x (#apply, #operation and friends). The template never passes user input there, but if your own code builds variants from request parameters, staying on 1.x would leave you exposed.