Upload multiple files to S3 with Refile

Chủ nhân của Carrierwave, Jonas Nicklas gần đây đã release một gem mới có tên Refile, theo bài viết của anh trên elabs thì Refile được gọi là CarrierWave's killer, Jonas Nicklas sẽ giải thích vì sao anh tạo Refile để thay thế cho Carrierwave.

Refile sẽ có nhiều điểm vượt trội như: đơn giản hơn, upload trực tiếp lên S3 chỉ với 1 config là xong, không phải quan tâm đến file được lưu ở đâu và như thế nào...

Project Refile được khởi động vào khoảng cuối năm 2014, nó đã release 5 version tính đến thời điểm hiện tại.

Bài viết sau hướng dẫn sử dụng gem Refile để upload multiple files, trường hợp 1 file thì tương tự hướng dẫn README trên source code page, và đề cập đến những tính năng thiếu của nó do tuổi đời còn non trẻ.

Gemfile

gem "refile", require: ["refile/rails"]

Ví dụ được đưa ra ở đây là product has_many :picturespicture belongs_to :product, và picture attachment :file

Nếu khai báo attachment :file thì bạn cũng add file_id, cũng như metadata của file vào như: file_filename, file_size, file_content_type. Refile sẽ tự động điền metadata khi bạn tạo object.

Migration

class CreatePictures < ActiveRecord::Migration
  def change
    create_table :pictures do |t|
      t.string :product_id
      t.string :file_id
      t.string :file_filename
      t.integer :file_size
      t.string :file_content_type

      t.timestamps
    end
  end
end
class Picture < ActiveRecord::Base
  belongs_to :product
  attachment :file, content_type: ["image/jpeg", "image/png", "image/gif"]

  validates_uniqueness_of :file_filename
end

class Product < ActiveRecord::Base
  UPDATABLE_ATTRIBUTES_FOR_ADMINS = [:name, :description,
    :price, pictures_attributes: [:id, :file, :_destroy]]
  has_many :pictures
  accepts_nested_attributes_for :pictures, allow_destroy: true

  def create_pictures! picture_params
    transaction do
      picture_params.each do |picture_param|
        pictures.create!(file: picture_param)
      end
    end
  end
end

Ở đây, ta dùng nested attributes của Rails để có thể submit một form với nhiều pictures, thêm pictures_attributes: [:id, :file, :_destroy] vào permited params.

:_destroy ở đây là tham số của nested attributes, nếu :_destroy = 1 thì nested attribute picture cũng như file sẽ bị xoá. Bạn không cần phải dùng đến tham số :remove_file như attribute bình thường nữa.

Form


<%= form_for [:admin, @product], html: {multipart: true} do |f| %>
  <%= f.fields_for :pictures do |builder| %>
    <%= builder.object.file_name %>
    <%= builder.check_box :_destroy %>
  <% end %>
  <%= file_field_tag "pictures[]", multiple: true %>
  <%= f.submit %>
<% end %>

Controller


def create
  if @product.save
    if params[:pictures].present?
      begin
        @product.create_pictures! params[:pictures]
      rescue ActiveRecord::RecordInvalid
        redirect_to [:admin, @product] and return
      end
    end
    redirect_to [:admin, @product]
  else
    render :new
  end
end

Hàm update trong controller cũng viết tương tự như create như trên.

Backend


require "refile/backend/s3"

aws = {
  access_key_id: "#{ENV["AWS_ACCESS_KEY_ID"]}",
  secret_access_key: "#{ENV["AWS_SECRET_ACCESS_KEY"]}",
  bucket: "#{ENV["AWS_BUCKET"]}",
}
Refile.cache = Refile::Backend::S3.new(prefix: "cache", **aws)
Refile.store = Refile::Backend::S3.new(prefix: "store", **aws)

Khi upload một file lên S3, trước khi record được validate và save thì file nằm ở trong Refile.cache nếu không có lỗi thì file sẽ được chuyển sang Refile.store. Ngược lại, file vẫn nằm ở Refile.cache để submit lại hoặc để cleanup sau này.

Để cài đặt biến môi trường bạn có thể dùng gem dotenv hoặc figaro

Còn phần upload 1 file bạn có thể đọc README trên project page

Hiện tại Refile có một số tính năng để upload 1 file rất hay như:

  • Presigned uploads: Upload trực tiếp lên S3 bucket mà không qua ứng dụng của bạn. Chỉ cần configure CORS và thêm presigned: true vào upload form.
  • Direct upload: Upload ngay và luôn khi bạn vừa chọn file xong nhằm giảm thời gian chờ đợi. Chỉ cần thêm //= require refiledirect: true
  • Đặc biệt, đối với những file có dung lượng > 100MB, file được chia nhỏ ra từng phần có dung lượng 5MB, từng phần được upload lên S3 sau đó việc ghép nối cũng được thực hiện tự động (tham khảo thêm Amazon S3: Multipart Upload). Mình đã tiến hành so sánh việc upload multiple files với Refile như trên và Carrierwave, kết quả là Refile luôn upload nhanh hơn từ 2-3 lần so với Carrierwave (15 files dung lượng khác nhau với tổng dung lượng khoảng 500MB, S3 standard storage, đo bằng tay, bạn nào biết gem gì có thể đo cái performance cho Rails 4 này thì bảo mình với 😄)
  • Image processing với mini_magick
  • Các tính năng khác: validate file type, remove file, helper để hiển thị ảnh

Một số tính năng Refile còn thiếu và có định hướng phát triển:

  • Upload multiple files directly and normally.
  • Refile luôn truyền file qua app nên nó không thể tạo URLs cho file của bạn được. Bạn có thể thêm CDN hoặc một HTTP cache nằm trên tầng app. Tuy nhiên nếu bạn thực sự cần lấy URL của file thì bạn có thể dùng cách sau:
  • Refile dùng SecureRandom.hex(30) để tạo ra id cho file nhằm tránh trùng lặp, điều này rất tiện cho việc xử lý trong gem tuy nhiên nếu bạn đang viết API và bạn dùng Refile, bạn muốn response là list file URL thì response trông không được đẹp cho lắm 😄

def file_url
  uri = file.to_io.base_uri
  scheme = uri.scheme
  host = uri.host
  request_uri = uri.request_uri
  "#{scheme}://#{host}#{request_uri}"
end

My source code: https://github.com/nhattan/hbc/pull/16/files