Membuat uploader asset di Rails
- Install libvips dan libvips-dev
- Install gem image_processing
- Iinstall gem aws-sdk-s3
Untuk no. 1 dan no. 2, diperlukan jika kita akan melakukan unggah gambar dan melakukan pemrosesan, seperti resize ukuran atau melakukan penyesuaian kualitas.
Untuk no. 3; bukan berarti saya akan pakai S3 AWS. Tetapi SDK ini bisa berfungsi untuk penyimpanan yang mirip seperti S3 AWS.
Tadinya saya menggunakan SDK ini karena berniat memakai DigitalOcean Spaces, tetapi ternyata ada yang lebih efisien secara harga (R2 Cloudflare).
Tutorial lengkap, bisa Anda pelajari di dokumentasi Rails Active Storage.
Rencananya di tulisan ini saya akan membuat general attachment uploader. Untuk UI uploader, saya memakai Active Admin.
Menyiapkan R2 Bucket
Agar Active Storage bisa bekerja semestinya. Kita perlu memberikan permission, GET, PUT, dan DELETE.
[
{
"AllowedOrigins": [
"http://localhost:4000",
"https://nadiar.id"
],
"AllowedMethods": [
"GET",
"POST",
"DELETE",
"PUT",
"HEAD"
],
"AllowedHeaders": [
"Content-Type",
"Content-MD5",
"Content-Disposition"
]
}
]
Anda juga bisa memberikan permission spesifik untuk bucket yang dipakai saja.
Rails Configuration
Untuk bisa berkomunikasi dengan R2, kita perlu memberikan access key kepada Rails. Menariknya di Rails semua sudah disediakan, tidak perlu ada gem yang kita pasang.
Buka config/storage.yml
, Anda bisa menembahkan access key di sana.
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
cloudflare:
service: S3
bucket: <%= ENV.fetch("BUCKET_NAME") %>
region: auto
endpoint: <%= ENV.fetch("R2_ENDPOINT") %>
access_key_id: <%= ENV.fetch("CF_ACCESS_KEY_ID") %>
secret_access_key: <%= ENV.fetch("CF_ACCESS_KEY") %>
upload:
cache_control: "public, max-age=31536000"
Attachment Model
Pertama, saya akan menyiapkan satu table khusus yang akan melisting semua file
class CreateAttachments < ActiveRecord::Migration[7.1]
def change
create_table :attachments do |t|
t.string :name, null: false
t.string :description
t.timestamps
end
end
end
Selanjutnya aktivasi Active Storage dan model.
$ bin/rails active_storage:install
$ bin/rails db:migrate
Di bagian model, saya hanya memberikan validasi name agar tidak kosong; dan setiap attachment ini memiliki satu attached file.
# frozen_string_literal: true
# == Schema Information
#
# Table name: attachments
#
# id :bigint not null, primary key
# name :string not null
# description :string
# created_at :datetime not null
# updated_at :datetime not null
#
class Attachment < ApplicationRecord
has_one_attached :attachment
validates :name, presence: true
end
Nama :attachment
bisa apa saja. Di belakang, model ini akan berelasi dengan polymorphic tabel active_storage_attachments
.
Sampai sini saja sebenarnya sudah cukup. Tetapi nantinya kita akan unggah attachment file di root bucket dengan nama random tanpa extension [baca].
Karena alasan itu, kita akan mengganti default key untuk berkas yang akan diunggah. Formatnya attachments/{key}/{filename}
.
# frozen_string_literal: true
# == Schema Information
#
# Table name: attachments
#
# id :bigint not null, primary key
# name :string not null
# description :string
# created_at :datetime not null
# updated_at :datetime not null
#
class Attachment < ApplicationRecord
has_one_attached :attachment
before_validation :set_attachment_file_name
validates :name, presence: true
private
def set_attachment_file_name
filename = attachment.filename
key = attachment.key
attachment.key = "attachments/#{key}/#{filename}"
end
end
Uploader UI
Saya memakai Active Admin, jadinya tidak begitu memusingkan tentang UI. Dengan DSL Active Admin, kita bisa generate form uploader sesuai dengan model yang kita buat.
# frozen_string_literal: true
include ActionView::Helpers::NumberHelper
ActiveAdmin.register Attachment do
config.filters = false
permit_params :name, :description, :attachment
index do
selectable_column
id_column
column :name
column :cdn_url do |o|
url = cdn_url o.attachment
link_to url, url
end
column :file_type do |o|
o.attachment.content_type
end
column :file_size do |o|
number_to_human_size o.attachment.byte_size
end
column :created_at
column :updated_at
actions
end
show do |o|
attributes_table do
row :id
row :name
row :description
row :cdn_url do
url = cdn_url o.attachment
link_to url, url
end
row :file_type do
o.attachment.content_type
end
row :file_size do
number_to_human_size o.attachment.byte_size
end
row :created_at
row :updated_at
end
end
form do |f|
f.inputs do
f.input :name
f.input :description
f.input :attachment, as: :file
end
f.actions
end
end
Bonus: Selain form, saya menambahkan informasi file size dan mime type.
Router
Untuk membedakan url R2 dan lokal, kita perlu memberi tahu Rails melalui routes.rb
.
direct :cdn do |blob|
if Rails.env.development? || Rails.env.test?
route_for(:rails_blob, blob)
else
File.join(ENV.fetch('CDN_HOST'), blob.key)
end
end
Hidupkan ulang Rails untuk take effects. Selanjutnya untuk mengetahui apa URL dari attachment, sekarang kita bisa menggunakan helper cdn_url
. Contohnya seperti di bawah:
latest_asset = Attachment.last.attachment
Rails.application.routes.url_helpers.cdn_url latest_asset
# https://cdn.nadiar.id/attachments/p7gv8ejr2vtq1gjv6xyrouas4aai/Screenshot from 2024-02-24 20-29-52.png
Lebih lengkapnya untuk penjelasan bagian routing CDN, Anda bisa membacanya di blog Florin Lipani.
Kesimpulan
Jika dibandingkan dengan Carrierwave, saya lebih memilih Active Storage. Alasannya karena penggunaanya yang sederhana. Saya tidak perlu membuat uploader helper untuk setiap model. Selain itu Active Storage sudah di-include dari Rails tanpa harus install gem 3rd party.