[Ruby On Rails][Carrierwave] The solution for preventing the upload with dupplicate file name

Gem carrierWave có lẽ không còn xa lạ với cộng đồng Ruby on Rails Developer. Nó cùng với paperclip là 2 gem được sử dụng phổ biến nhất trong việc upload file. Tuy nhiên trong quá trình upload file, vấn đề mà có lẽ bất kì developer nào cũng gặp phải là việc dupplicate tên file. Để xử lý được vấn đề này, có những giải pháp nào, chúng ta cùng tìm hiểu qua các mục dưới đây.

Option 1: Creating Random and Unique File Names in CarrierWave.

Đây có lẽ là giải pháp đơn giản nhất mà ai cũng nghĩ tới đầu tiên. Và bên dưới đây là cách để thực hiện nó.

Unique filenames

Chúng ta có thể generate UUID filenames có format sau: 1df094eb-c2b1-4689-90dd-790046d38025.jpg someversion_1df094eb-c2b1-4689-90dd-790046d38025.jpg

class PhotoUploader < CarrierWave::Uploader::Base
  def filename
    "#{secure_token}.#{file.extension}" if original_filename.present?
  end

  protected
  def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid)
  end
end

Random filenames

Chúng ta có thể generate hexadecimal filenames có format sau: 43527f5b0d.jpg someversion_43527f5b0d.jpg

class PhotoUploader < CarrierWave::Uploader::Base
  def filename
     "#{secure_token(10)}.#{file.extension}" if original_filename.present?
  end

  protected
  def secure_token(length=16)
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var, 	SecureRandom.hex(length/2))
  end
end

Thêm một lưu ý nhỏ, khi thực hiện việc mã hóa tên file, mà bạn vẫn muốn giữ lại tên gốc để phục vụ cho các mục đích khác, thì có thể thực hiện như sau:

  # in `class PhotoUploader`
  before :cache, :save_original_filename
  def save_original_filename(file)
    model.original_filename ||= file.original_filename if file.respond_to?(:original_filename)
  end

Các đoạn code trên hoàn toàn đơn giản, dễ hiểu và rất phổ biến đúng không nào? Giải pháp này thực sự rất OK, tuy nhiên, khi bạn muốn hiển thị lỗi cho người dùng và buộc người dùng phải tạo lại tên file để nó là duy nhất thì sao? Chúng ta cùng tìm hiểu thử giải pháp 2.

Option 2: Using Model Validations to Prevent Duplicate File Names in CarrierWave.

Làm sao để validate file_name:

validates :file_name, :uniqueness => true

Bằng cách này được không? Câu trả lời là không? Nó sẽ lập tức bắn ra lỗi:

can’t cast PhotoUploader to string

Lỗi trên được gây ra bởi file_name trả về 1 thể hiện của Uploader (Trong trường hợp này là PhotoUploader ) chứ không phải là chuỗi cuối cùng được lưu trong DB. Vậy thực hiện được phương pháp 2 này bằng cách nào: Đơn giản là trong class PhotoUploader ta viết thêm một hàm để validate: validate_file_name_is_unique.

validate :validate_file_name_is_unique

private

def validate_file_name_is_unique
    if UploadedFile.where(:file_name => file_name.file.original_filename).count > 0
         errors.add :file_name, "'#{file_name.file.original_filename}' already exists"
    end
end

Nhìn thì có vẻ OK rồi đấy. Tuy nhiên nó sẽ gặp vấn đề nghiêm trọng khi: 2 người dùng cùng cố gắng upload cùng 1 lúc với cùng 1 tên file. Điều kiện kiểm tra count > 0 sẽ trả về true cho cả 2 người dùng, dẫn đến họ đều có thể upload được file.

Option 3: Using A Uniqueness Constraint on the Database to Prevent Duplicate File Names in CarrierWave.

Đầu tiên, chúng ta tạo ra 1 migration với chỉ mục duy nhất (:unique => true)

class AddUniqueConstraintToFileNameOnPhoto < ActiveRecord::Migration
     def change
         add_index :photos, :file_name, :unique => true
     end
end

Nếu điều này này bị vi phạm trong quá trình tải tệp lên, CarrierWave sẽ ngay lập tực chặn việc upload file lên máy chủ. Tiếp theo, chúng ta có thể sử dụng Rails sẽ ném ra error ActiveRecord :: RecordNotUnique khi ràng buộc :unique => true bị vi phạm. Và sử dụng begin rescue end để bắt lỗi đó, để thực hiện việc đó có một số options sau:

Rescuing In the Controller (Not Advised)

Trong PhotosController, ta làm như sau:

class PhotosController < ApplicationController
	def create
     	@photo = Photo.new(protected_params)
     begin
          success = @photo.save
     rescue ActiveRecord::RecordNotUnique => e
          success = false
          @photo.errors.add :file_name, "'#{@photo.file_name.filename}' already exists"
     end

     if success
          #redirect somewhere
     else
          #render something
     end
end

Nói chung, cách trên khuyến khích là khong nên dùng. Do nó phá vỡ tính đóng gói, controllers đảm nhận việc validating models và quản lý danh sách lỗi của models( Đáng lẽ nhiệm vụ này thuộc về models). Hơn nữa, với cách trên bạn phải thêm n khối begin rescue end vào n function nếu nó làm thay đổi tên file và lưu nó vào DB. Điều này làm tăng độ phức tạp, và tốt hơn hết là nên để model thực hiện công việc trên.

Rescuing in the Model (Recommended).

class Photo < ActiveRecord::Base
 	def save
          super
     rescue ActiveRecord::RecordNotUnique => err
          errors.add :file_name, "'#{file_name.file.original_filename}' already exists"
          false  #the save method must return true or false
     end
end

Kết luận.

Có nhiều options khác nhau cho bạn lựa chọn để ngăn chặn việc upload với tên file bị trùng. Tùy vào mục đích sử dụng thì lựa chọn giải pháp cho hợp lý.

Tài liệu tham khảo:

1, https://corlewsolutions.com/articles/article-1-prevent-uploads-with-duplicate-file-name-in-carrierwave-and-rails 2, https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Create-random-and-unique-filenames-for-all-versioned-files