Membedah Active Storage untuk file upload: attachments, image variants, storage services (lokal & S3), dan direct upload, plus praktik upload avatar dengan image processing

Di episode 12 kalian mengoptimasi cache dan query; episode 13 membahas topik yang sering disepelekan namun rawan error: file upload lewat Active Storage. Upload bukan sekadar menyimpan file — melibatkan validasi tipe, pemrosesan gambar, keamanan, dan pilihan storage yang menyesuaikan skala aplikasi.
Mengapa episode ini penting? Karena aplikasi modern hampir selalu melibatkan upload: avatar, lampiran, dokumentasi, media. Active Storage membakukan semuanya: API konsisten antar storage backend, variants untuk gambar, dan direct upload yang memindahkan beban ke browser. Kesalahan kecil di sini (misal menerima file tak diinginkan) bisa menjadi celah keamanan — termasuk yang diserang pada CVE-2026-66066.
Active Storage adalah modul bawaan Rails yang menghubungkan model ke file di berbagai storage:
class User < ApplicationRecord
has_one_attached :avatar
end
class Post < ApplicationRecord
has_many_attached :images
endhas_one_attached untuk satu file, has_many_attached untuk koleksi. File disimpan di tabel active_storage_blobs, relasi ke record di active_storage_attachments, dan hasil transformasi gambar di active_storage_variant_records.
bin/rails active_storage:install
bin/rails db:migrateInstalasi menambahkan tabel-tabel di atas.
Semua storage backend dideklarasikan di config/storage.yml:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
amazon:
service: S3
access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %>
secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %>
region: ap-southeast-1
bucket: blog-app-productionLalu pilih service aktif per environment:
config.active_storage.service = :localconfig.active_storage.service = :amazon| Service | Backend | Cocok Untuk |
|---|---|---|
| Disk | File system lokal | Development, VPS kecil |
| S3 | Amazon S3 | Production skala web |
| Mirror | Kombinasi service | Backup/redundansi otomatis |
Satu baris konfigurasi mengganti seluruh storage — kode aplikasi tidak berubah. Ini kekuatan utama Active Storage.
Attachments dipakai langsung di model:
user.avatar.attach(io: File.open("avatar.png"), filename: "avatar.png", content_type: "image/png")
user.avatar.attached? # true
user.avatar.purge # hapus blob + attachmentValidasi ukuran dan tipe dilakukan di model — Active Storage tidak memvalidasi otomatis:
class User < ApplicationRecord
has_one_attached :avatar
validate :avatar_type_and_size
private
def avatar_type_and_size
return unless avatar.attached?
if !avatar.content_type.in?(%w[image/png image/jpeg image/webp])
errors.add(:avatar, "harus PNG, JPEG, atau WebP")
elsif avatar.byte_size > 5.megabytes
errors.add(:avatar, "maksimal 5 MB")
end
end
endSelalu batasi tipe dan ukuran — menerima file sembarang adalah pintu masuk masalah (skrip jahat, file raksasa yang memenuhi disk).
Tip
Attach dari form <%= f.file_field :avatar %> di form file (sekarang default Rails 8) mengirim multipart. Rails menempatkan upload ke params[:user][:avatar] — user.avatar.attach(params[:user][:avatar]) di controller.
Upload gambar sering butuh versi turunan (thumbnail, crop). Active Storage memakai gem image_processing dengan libvips (default Rails 8):
<%= image_tag user.avatar.variant(resize_to_limit: [200, 200]) %>variant menghasilkan blob turunan yang di-cache di active_storage_variant_records; pemrosesan on-demand saat diminta, dan memanggil .processed lebih awal agar request pertama tidak lambat. Gunakan resize_to_limit (skala turun, jaga rasio) atau resize_to_fill (crop persis). Pastikan image_processing & libvips selalu diperbarui — celah di library pemrosesan gambar (seperti CVE-2026-66066) dieksploitasi lewat gambar jahat.
Untuk file besar, kirim langsung ke storage dari browser tanpa lewat app server:
<%= form.file_field :avatar, direct_upload: true %>Browser meng-upload file ke storage via signed URL sementara (/rails/active_storage/direct_uploads), lalu app hanya menerima blob key — beban pindah dari server ke client, request tidak memblokir worker, dan app server tidak membawa payload raksasa.
Skema lengkap upload avatar:
<%= form_with model: @user do |f| %>
<%= f.label :avatar %>
<%= f.file_field :avatar, direct_upload: true, accept: "image/*" %>
<% if @user.avatar.attached? %>
<%= image_tag @user.avatar.variant(resize_to_limit: [100, 100]) %>
<% end %>
<%= f.submit %>
<% end %>def update
@user.avatar.attach(params[:user][:avatar]) if params[:user][:avatar].present?
...
endaccept: "image/*" di sisi form hanya UX — validasi server (tipe + ukuran) tetap wajib.
variant tanpa processed — gambar lambat di request pertama; proses lebih awal atau di background.:test.storage.yml — gunakan ENV vars / credentials (episode 16); jangan commit access key.purge saat banyak variant — hapus via purge_later agar berjalan di background (disk tidak diblokir).Episode 13 membekali kalian Active Storage: attachment model (has_one_attached/has_many_attached), storage services dengan API seragam (Disk/S3/GCS/Mirror), validasi tipe & ukuran, image variants dengan libvips, dan direct upload yang memindahkan beban ke browser — plus praktik upload avatar end-to-end.
Inti yang harus dibawa pulang:
accept di form hanyalah UX.variant(resize_to_limit: ...) untuk thumbnail; processed agar tidak lambat di request pertama.config/storage.yml — kode aplikasi tak berubah.Di episode 14 selanjutnya kita akan membedah Action Cable & realtime — WebSocket channels, streaming, dan integrasi dengan Turbo Streams untuk chat & notifikasi realtime. Sampai jumpa di episode 14!