BlogNotesAbout
moon indicating dark mode
sun indicating light mode

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 < ApplicationRecord
mount_uploader :avatar, AvatarUploader
end

which will use this uploader:

class AvatarUploader < CarrierWave::Uploader::Base
storage :file
end

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 = :file
config.enable_processing = false
end
else
...
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 of https://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

<img
src="/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:

  1. 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.
  2. 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:

<img
src="/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!


Brandon Conway
I enjoy learning about and writing code in many programming languages