How to avoid using the base CarrierWave uploader class on accident
March 05, 2020
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.
Create a Common 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 include
d CarrierWave::VIPS
in that class.
Side Note
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 placeif (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}"endend
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
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 CarrierWavemodule Uploaderendend
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 CarrierWavemodule Uploadermodule YourAppNameclass MustInheritFromApplicationUploaderError < StandardError; enddef 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?"endsuperendendendend
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 CarrierWavemodule Uploader# ...class Baseprepend Uploader::YourAppNameendendend
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 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 allclass 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!