Blog index

Membuat uploader asset di Rails

Dibuat pada:
  1. Install libvips dan libvips-dev
  2. Install gem image_processing
  3. 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.

Cloudflare R2 permission

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.

Cloudflare R2 permission

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.