Setting up carrierwave file uploads using GridFS on Rails 3 and mongoid

I recently needed to set up a prototype on Heroku using MongoDB. After digging into the topic for some time I finally got it working. Hence this guide.

Since Heroku offer an Add-On for configuring your app with MongoHQ database, I immediately went for that and decided to use Mongoid. However, once you want to use file uploads with e.g. carrierwave, you realize that Heroku’s file system is read-only. You can go for Amazon S3 storage, but that’ll cost you extra. But wait - if you are using MongoDB, why not settle for GridFS? I found a tutorial written by Matsimitsu on jkreeftmeijer's blog, but employing MongoMapper and Rails 2.3.x made it obsolete and incompatible with my needs.

Setting up and configuration

I won't go into details of setting up either a Heroku app or a MongoHQ database, everything is available on their websites. After you set up a Rails 3 app, be sure to include the following in your Gemfile:

#Gemfile

gem 'carrierwave'
gem 'mongoid', '2.0.0.beta.19'

Configuring the database connection

To use the MongoHQ database instead of Heroku's default PostgreSQL instance, you need to configure the production config/mongoid.yml using MongoHQ connection details:

#config/mongoid.yml

production:
host: example.mongohq.com
port: 27063
username: mongo
password: password
database: database

File uploads

I chose carrierwave over Paperclip, since Paperclip is not that easy to get working with Mongoid, as e.g. @meskyanichi found out.

In order to get carrierwave properly working using the remote GridFS storage, you need the following configuration in it’s initializer.

#initializers/carrierwave.rb

CarrierWave.configure do |config|
  config.storage = :grid_fs

  # Same as your MongoHQ database connection parameters
  config.grid_fs_connection = Mongoid.database

  # Storage access url
  config.grid_fs_access_url = "/grid"
end

Update: Shorter config thanks to (shorter thanks to @joshkalderimis)

Your model will probably look something like this:

#app/models/player.rb

class Player
  include Mongoid::Document

  mount_uploader :avatar, AvatarUploader

  field :name
end

And the corresponding uploader:

#app/uploaders/avatar_uploader.rb

class AvatarUploader < CarrierWave::Uploader::Base

  storage :grid_fs

  def store_dir
    "#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
end

Now carrierwave will store images properly, but for your Rails app to serve images from the Mongo GridFS storage using the carrierwave image url methods, you need to process those requests separately. Matsimitsu used Rails Metal for this purpose, but since Metal has been removed from Rails 3, you need to use separate Rack middleware. Here’s the Rack code for serving images from GridFS when routed to the /grid (same as in the carrierwave initializer) path:

#lib/serve_gridfs_image.rb

class ServeGridfsImage
  def initialize(app)
    @app = app
  end

  def call(env)
    if env["PATH_INFO"] =~ /^\/grid\/(.+)$/
      process_request(env, $1)
    else
      @app.call(env)
    end
  end

  private

  def process_request(env, key)
    begin
      Mongo::GridFileSystem.new(Mongoid.database).open(key, 'r') do |file|
        [200, { 'Content-Type' => file.content_type }, [file.read]]
      end
    rescue
      [404, { 'Content-Type' => 'text/plain' }, ['File not found.']]
    end
  end
end

Don’t forget to enable this middleware in your Rails app application.rb config file, by adding it to the config.middleware parameter:

#config/application.rb

config.middleware.use "ServeGridfsImage"

Boom!

This config should be enough for both uploading and serving file uploads for your app using separate GridFS storage e.g. on Heroku. I hope you’ll find it useful. If you have any questions or suggestions on how to make it simpler, be sure to put them down in the comments!