Tuesday, October 4, 2011

User Avatars on Cloud Foundry

Most web applications today have the need to upload and serve user generated images such as profile pictures, photos or video thumbnails. If you are using Ruby on Rails there are a couple great frameworks you can use: Namely PaperClip and CarrierWave.

After reviewing both libraries I went with CarrierWave for this project as it seemed the least obtrusive and most flexible. CarrierWave can be used with the file system, Amazon S3 and a database including Mongo GridFS. Since my project is hosted on Cloud Foundry and that gives me free access to install Mongo and bind it to my Application so I decided to try that option.
The first task for which I wanted to add images was when users registered on my app using their Facebook account. Here are the steps you can take to support uploading and serving images. For more details on the Facebook integration review the user.rb model in the source code.

Steps on your terminal

This assumes you already have a Ruby on Rails 3.0 application on Cloud Foundry with a Users model
# Log in to cloud foundry if you are not logged in
vmc login youremail@website.com

vmc create-service mongodb

# See what the newly created mongo service is called
vmc services

# Bind the service to your existing Application
vmc bind-service mongodb-???? appname

Steps on your Code base

1- Add gems to your Gemfile

gem 'carrierwave'
gem 'carrierwave-mongoid', :require => "carrierwave/mongoid"

2- Install CarrierWave for your Model

rails generate uploader Avatar

3 - Edit the generated file

app/uploaders/avatar_uploader.rb
to contain:
class AvatarUploader < CarrierWave::Uploader::Base

          # Choose what kind of storage to use for this uploader:
          storage :grid_fs

          # Override the directory where uploaded files will be stored.
          # This is a sensible default for uploaders that are meant to be mounted:
          def store_dir
              "#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
          end

          # Provide a default URL as a default if there hasn't been a file uploaded:
          def default_url
              "/images/fallback/" + [version_name, "default.png"].compact.join('_')
          end
      end

4- Update your ActiveRecord model to store the avatar

Make sure you are loading CarrierWave after loading your ORM, otherwise you'll need to require the relevant extension manually, e.g.:
require 'carrierwave/orm/activerecord'
Add a string column to the model you want to mount the uploader on:
add_column :users, :avatar, :string
Open your model file and mount the uploader:
class User
  mount_uploader :avatar, AvatarUploader

  # Make sure that the avatar is accessible
  attr_accessible :avatar, :remote_avatar_url, :email, :password, :password_confirmation, :remember_me, :first_name, :last_name, :display_name, :username ...

.
end

5 - Create an initializer for Mongoid to use your Mongo DB instance on Cloud Foundry

  • Name it 01_mongoid.rb so it runs before everything else
Mongoid.configure do |config|
  conn_info = nil

  if ENV['VCAP_SERVICES']
    services = JSON.parse(ENV['VCAP_SERVICES'])
    services.each do |service_version, bindings|
      bindings.each do |binding|
        if binding['label'] =~ /mongo/i
          conn_info = binding['credentials']
          break
        end
      end
    end
    raise "could not find connection info for mongo" unless conn_info
  else
    conn_info = {'hostname' => 'localhost', 'port' => 27017}
  end

  cnx = Mongo::Connection.new(conn_info['hostname'], conn_info['port'], :pool_size => 5, :timeout => 5)
  db = cnx['db']
  if conn_info['username'] and conn_info['password']
    db.authenticate(conn_info['username'], conn_info['password'])
  end


  config.master = db
end

6 - Update your CarrierWave Initializer to use the Cloud Foundry Mongo DB

#initializers/carrierwave.rb
require 'serve_gridfs_image'

CarrierWave.configure do |config|
  config.storage = :grid_fs
  config.grid_fs_connection = Mongoid.database

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

7- Handle requests for the images in 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

Step 8 - Deploy !

bundle install
bundle package
vmc update app_name

Conclusion

This will give you the ability to upload and serve images. Do note that this will not provide image resizing. If you are using devise for example you can import the avatar(profile picture) of the user when they sign up.
class << self
    def new_with_session(params, session)
      super.tap do |user|
        if session['devise.omniauth_info']
          if data = session['devise.omniauth_info']['user_info']
            user.display_name = data['name'] if data.has_key? 'name'
            user.email = data['email']
            user.username = data['nickname'] if data.has_key? 'nickname'
            user.first_name = data['first_name'] if data.has_key? 'first_name'
            user.last_name = data['last_name'] if data.has_key? 'last_name'
            user.remote_avatar_url = data['image'] if data.has_key? 'image'
          end
        end
      end
    end
  end

References

social coder

My photo
San Francisco, California, United States
Open Web Standards Advocate