Photo by Edvard Alexander Rølvaag on Unsplash

How to avoid using the base CarrierWave uploader class on accident

If you forget to specify a custom uploader, CarrierWave will generate a class for you, which inherits from CarrierWave::Uploader::Base. This can cause issues, which you might not catch at first.

Unintended Consequences

Other issues, which are a little more obvious, come from the fact that you can’t override the base uploader with customizations. One common customization is including an image processor like CarrierWave-VIPS or CarrierWave::MiniMagick.

Another one is overriding methods, such as #filename and #store_dir.

In my experience, it is very important to be able to make these customizations. Unfortunately, it is difficult to specify an uploader once there is existing data for the implicit uploader (the one CarrierWave created automatically based on the base uploader).

How to Ensure You Don’t Forget to Specify an Uploader

Create a Common Uploader

Next, you should move all of your common code into this parent class. If all of your uploaders have duplicate code, this is where that duplicate code should go so that it can be inherited. In my case, I created a custom #store_dir method and included CarrierWave::VIPS in that class.

Side Note

def store_dir
# we don't want to use the old "uploads" default after we fixed
# this issue of uploading images to the wrong place
if (model&.created_at || Time.now) < Time.parse("2019-12-08 22:19:45 UTC")
"uploads"
else
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end

Any of your uploads that already went to the wrong place will need to continue pointing to that directory. Otherwise, they will all be broken links/URLs. I specified the timestamp when I deployed to production as the dividing line where I started using the correct upload location.

Enforcing the Use of the New Class

Open your CarrierWave configuration file (or else create one). For my Rails application, that is config/initializers/carrierwave.rb.

Next, open the CarrierWave::Uploader module, like this:

module CarrierWave
module Uploader
end
end

We are going to create a new module, and include that new module in the base uploader class. Our module will have only one method, which is #initialize.

The #initialize method is going to check whether we are using the new uploader or not. If we are, then it will call super. If we are not, however, it will raise an error. This ensures that you can never accidentally forget to specify an uploader again, because your development will be stopped until you specify an uploader.

Here is what that looks like:

module CarrierWave
module Uploader
module YourAppName
class MustInheritFromApplicationUploaderError < StandardError; end
def initialize(model = nil, mounted_as = nil)
# If this is raised, it is a reminder that the applied
# uploader needs to inherit from ApplicationUploader. We
# likely didn't specify an uploader in the object model
# file.
unless is_a?(ApplicationUploader)
raise MustInheritFromApplicationUploaderError, "Did you forget to specify an uploader?"
end
super
end
end
end
end

The method must have the same signature as the one we are overriding, so that the call to super does not fail.

Next, we include the module (using prepend, so that our module is placed ahead of the class itself in the lookup chain):

module CarrierWave
module Uploader
# ...
class Base
prepend Uploader::YourAppName
end
end
end

That’s it! From now on, if your application tries to mount an uploader that does not inherit from ApplicationUploader, an error will be raised.

Bonus: Dealing With Legacy Data

# This class is to be used in places where we did not previously
# specify an uploader at all
class LegacyUploader < ApplicationUploader; end

As long as everything in ApplicationUploader is safe for existing data, then this will work fine. At any time, I can override ApplicationUploader here with legacy-safe code.

Thank You, Friend

Have a great day!

Originally published at https://brandoncc.dev.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store