[Ruby On Rails][Carrierwave] The solution for preventing the upload with dupplicate file name
Bài đăng này đã không được cập nhật trong 7 năm
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
All rights reserved