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

The first issue (and the one that caused me the most grief) is that CarrierWave will upload to the uploads directory by default. This means that if you store your files on s3, all of your uploader’s uploads will end up in your-bucket.s3.amazonaws.com/uploads/. This is almost certainly not what you want.

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

The way you ensure that you don’t fall into this trap, is to force yourself to specify an uploader.

The first step is to create a custom uploader that all of your other uploaders will inherit from. In my application, I called it ApplicationUploader. Once you have created it, don't forget to change the parent class for all of your other uploaders.

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.

If you have already fallen prey to uploads that landed in “uploads”, you can create a #store_dir method like mine:

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.

Now that we have a new parent uploader, we want to make sure that we always use it. In order to do that, we are going to open the CarrierWave::Uploader::Base class and make some changes.

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.

This is all well and great, but what about places in your code that did not specify an uploader before? We need a safe way to modify them. I went with a new uploader called LegacyUploader. Here is what it looks like:

# 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

Thanks for taking the time to read (or skim) this article. I hope you found it helpful.

Have a great day!

Originally published at https://brandoncc.dev.