[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 đề ...
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.
Đâ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.
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.
Đầ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
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ý.
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