Services Spotlight:

Prevent Uploads With Duplicate File Name In CarrierWave and Rails

Written By Corlew Solutions
Updated February 3, 2014
Published January 15, 2014
Why Corlew Solutions?

We write great software and provide amazing technical support. Contact us today if you need help with a software project:

  • Website: Send an inquiry
  • Email:
  • Phone: (703) 688-3058

Article Technology Info

This article discusses the following technologies:

  • CarrierWave - A Ruby gem for managing file uploads.
    (github | docs | wiki)
  • CarrierWave 2.15.5 - A Ruby gem for managing file uploads.
    (github | docs | wiki)
  • Ruby on Rails - An open source web application framework written in Ruby.
    (website | docs)

CarrierWave is a great open source tool for managing file uploads in a Web Application. It has become a great resource for Ruby on Rails developers who need to give users the ability to upload and process files dynamically. In a web application we recently built, the design required us to place all uploaded files in a single folder with no sub folders. As you can imagine, this dramatically increased the possibility users might upload files with the same name and cause the universe to implode upon itself. The madness had to be stopped and this article presents a solution.

Option 1: Creating Random and Unique File Names in CarrierWave

The CarrierWave wiki page has an article that covers changing the file name of an uploaded file to something that is unique by adding a random string. This solution is good if you just want the form to happily accept the uploaded file and then handle conflicts quietly. But what if you want to display an error to the user and force him to create a unique file name?

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

In our particular application, every file that gets uploaded is associated with an UploadedFile object which holds details about the file. Our table has a :file_name column in it and we mount CarrierWave on it in the standard way:

1
2
3
class UploadedFile < ActiveRecord::Base
     mount_uploader :file_name, UploadedFileUploader
end

Our first thought was to simply add a uniqueness validation on the :file_name column:

1
validates :file_name, :uniqueness => true

This however won’t work and it will cause CarrierWave to generate the following error:

can’t cast UploadedFileUploader to string

This fails because the file_name method returns an instance of an Uploader (in our case UploadedFileUploader) instead of the string that ultimately gets stored in the database. As a result, the standard Rails validates method can’t help us; to get around this problem we just need to use a custom validator on the model:

class UploadedFile < ActiveRecord::Base
1
2
3
4
5
6
7
8
9
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

This solution works fine and may be as far as you want to take it. It does however suffer from one major flaw. If two users attempt to upload at the same time with the same file name, the count > 0 check could return true for both users, allowing both of them to upload the file. It may seem like an unlikely situation, but it can happen quite often in the right application.

To handle this situation we need to enforce the uniqueness constraint at the database level.

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

The first step is to add a migration with a unique index:

1
2
3
4
5
class AddUniqueConstraintToFileNameOnUploadedFiles < ActiveRecord::Migration
     def change
         add_index :uploaded_files, :file_name, :unique => true
     end
end

If this constraint is violated during the process of uploading the file, CarrierWave doesn’t upload the file to the server.

Next, we can take advantage of the fact that Rails will throw an ActiveRecord::RecordNotUnique error when the database tells it the unique constraint was violated. We can use a begin rescue end to catch the error. The only remaining question is where to put the rescue block - we have some options:

Rescuing In the Controller (Not Advised)

Typically, you’ll see begin rescue end> in the controller. Here is an implementation of the create action that handles everything:

class Admin::UploadedFilesController < ApplicationController
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def create
     @uploaded_file = UploadedFile.new(protected_params)

     begin
          success = @uploaded_file.save
     rescue ActiveRecord::RecordNotUnique => e
          success = false
          @uploaded_file.errors.add :file_name, "'#{@uploaded_file.file_name.filename}' already exists"
     end

     if success
          #redirect somewhere
     else
          #render something
     end
end

In general, this probably isn’t the best way to go. It breaks encapsulation to make controllers responsible for validating the model and managing the model’s errors list. Also, this technique requires adding the same begin rescue end block to every action that can change the file_name and save the model; this increases complexity. If possible, it’s far better to let the model handle it’s own errors and validate itself.

Rescuing in the Model (Recommended)

In the end this is the way we chose to go for our application. You do not need to create a custom validation or make any adjustments to the controller. All you have to do is define the following method on your model:

class UploadedFile < ActiveRecord::Base
1
2
3
4
5
6
     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

If you have several unique constraints on columns in your database, you will need to expand the rescue block a bit to determine which constraint was violated and then add the appropriate error message. Unfortunately, it doesn’t look there’s much of an API on the err object that gets passed in so you may have to resort to parsing the error string. If anyone can think of a better way to , please let us know and we’ll update the article.

We hope you found this article useful. If you see any mistakes, missing features or ways to improve it, please let us know in the comments below so we can update its contents. If you're willing to link to us, we would sincerely appreciate it!

Corlew Solutions is a Web Design and Web Application Development company based in Fairfax Virginia, minutes away Washington D.C. If you're looking for great web design from the Northern Virginia area or web design from the Washington D.C. area we can help. Contact Us today!

comments powered by Disqus