How to use Carrierwave's local file urls on the backend
February 23, 2020
Carrierwave is an excellent library for facilitating file uploads in Ruby on Rails. It comes with methods that work most of the time, for most users. What happens when you find yourself in a situation where those methods don’t fit your use case though?
You find a solution. I would like to share one of my solutions here.
Setting the Table
Imagine the following Ruby class:
class User < ApplicationRecordmount_uploader :avatar, AvatarUploaderend
which will use this uploader:
class AvatarUploader < CarrierWave::Uploader::Basestorage :fileend
Using #url
You can display your image using Carrierwave’s #url method in a view template very easily:
<%= image_tag @user.avatar_url, alt: "John's avatar" %>
which will result in something like this:
<img src="/uploads/user/avatar/11/avatar.jpg" alt="John's avatar" />
This is perfect, exactly what we want. Right?
Where It All Breaks Down
One day, your task is to add a feature to the backend which involves accessing the avatar images from your Ruby code.
You find that open(@user.avatar_url)
gives an error, saying that it can’t find a file on the file system located at /uploads/user/avatar/11/avatar.jpg
. That makes total sense, since the file actually lives at /home/deploy/app/public/uploads/user/avatar/11/avatar.jpg
.
Who Uses Local File Uploads Anyway?
Most likely, you. Do you have a Carrierwave initializer that looks like this?
if Rails.env.test?CarrierWave.configure do |config|config.storage = :fileconfig.enable_processing = falseendelse...end
If you do, then you are using local uploads in your test environment. This is exactly the reason I had an issue in my application.
What’s the Difference?
If you use a storage location like s3, then #url
will always return the HTTP (or HTTPS) URL for that resource. This URL is usable anywhere. On your backend, on your frontend, etc.
Local uploads are different though, since the browser needs to access the resource using one path, and your backend code needs an entirely different path.
The Solution
My initial solution was to override the #url
helper methods with a call to
#path
if the application was running in the test environment. #path
returns the location on the file system when called on an uploader object that
is stored on the local file system.
This fixed my issues in a passable way for years. All of the backend code worked
in development, staging, and production since #url
would return s3 urls, and
it worked in tests because it was overridden to return the result of a call to
#path
instead of #url
.
Not-So-Fun fact: If you call
#path
on an uploader that stored the file on s3, you get a relative path to the file. Instead ofhttps://s3.com/my-bucket/my-file.jpg
, you get/my-bucket/my-file.jpg
.This is exactly the problem that locally-stored files have, but in reverse! Because of that, using
#path
in tests and#url
everywhere else was a requirement.
Recently, Circle CI has starting experiencing failed tests because
<imgsrc="/home/deploy/app/public/uploads/user/avatar/11/avatar.jpg"alt="John's avatar"/>
results in a 404, and my Capybara configuration raises server errors for any errors that occur in the browser.
This is very unfortunate, although it was a good reason to finally come up with a solution that solved the problem for both the backend and the browser.
Options
I see two options:
- Get rid of the override that calls
#path
when in the test environment, causing us to use#url
in all environments. This would fix the browser 404s, but would return the backend to a broken state. - Fix the browser 404s.
The first option would require handling the paths in a special way in every usage on the backend. That is a lot to remember, and probably a lot more specialized code than just the one overridden method.
The second option would allow me to keep the single piece of specialized code, and also introduce a single piece of code that handled the 404s.
The Winner
For me, the winner was clear. Less specialized code is much better than more.
Here is a recap of the only issue that is occurring at this point:
<imgsrc="/home/deploy/app/public/uploads/user/avatar/11/avatar.jpg"alt="John's avatar"/>
This HTML results in a 404, which causes my test to fail.
Here is an example of one of those failures:
Failure/Error: raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"ActionController::RoutingError:No route matches [GET] "/home/deploy/app/public/uploads/user/avatar/1/sample_image.jpg"
The solution was given in this error. There is no route in Rails to handle this file. So, let’s add one!
I added this as the last code in the routes.rb
routes definitions:
if Rails.env.test?get "#{Rails.root.to_s}/public/:file_path",to: redirect('/%{file_path}'),constraints: { file_path: %r{uploads/.*} }end
That’s it. This route redirects any GET requests for
/home/deploy/app/public/uploads/user/avatar/11/avatar.jpg
to
/uploads/user/avatar/11/avatar.jpg
.
The browser is happy, and the backend is still happy as well.
Thank You, Friend
I hope this helps you as much as it helped me. This issue has been a terrible nuisance for years, and it is finally gone. I am sure I could have made more elaborate stubs to resolve the issue, but I prefer not to create and use magic code when possible. This simple solution works for me.
I hope you enjoyed this article, have a great day!