For the past few months I’ve been saying that the next app I make is going to use Google for authentication. I mean, seriously, who doesn’t have a google account? I knew it would be easy, as I’ve already written a gem to handle the authentication, so I thought I would whip it together quick and put it here for whoever finds it interesting. Onward!

Step 1: Initial Setup

To start with, we need a rails app (I’m using rails 2.1) and my google base authentication gem added as a dependency. First the app.

rails googleauth

Open up your newly created app and add the following to your environment file inside the Rails::Initializer block.

config.gem "googlebase", :lib => 'google/base', :version => '0.2'

Also, note that you should install the gem either with sudo gem install googlebase or rake gems:install.

Step 2: Controller and Login Form

Now that we have that, we need to have a controller to render the form and process the login. We are going to stick with the pretty standard sessions controller, with new showing the login form, create handling the processing of that form and destroy providing the logout functionality.

script/generate controller sessions new

We have the controller, now we need routes and some html. Add the following to your routes.

map.resource :sessions
map.login '/login', :controller => 'sessions', :action => 'new'
map.logout '/logout', :controller => 'sessions', :action => 'destroy'

And you’ll need a form to show the user:

<h1>Login</h1>
<%- form_tag sessions_path do -%>
    <p>We promise not to log or store your password in any way. It will be used only to authenticate with Google.</p>
    <ul>
        <li>
            <label for="username">Google Username</label>
            <%= text_field_tag 'username' %>
        </li>
        <li>
            <label for="password">Google Password</label>
            <%= password_field_tag 'password' %>
        </li>
        <li class="submit">
            <%= submit_tag 'Login' %>
        </li>
    </ul>
<%- end -%>

You’ll notice that we promised not to log or store their password. Let’s start with not logging it by uncommenting the following line in our application controller.

filter_parameter_logging :password

Next we’ll throw a bit of code in our newly generated sessions controller to get things working.

class SessionsController < ApplicationController
  def new
  end

  def create
    Google::Base.establish_connection(params[:username], params[:password])
  rescue Google::LoginError
    render :action => 'new'
  end

  def destroy
  end

end

If you start your server and visit http://localhost:3000/login, you should see a login form. If you put in your google username and an incorrect password, the form comes right back up. However, if you put in your username and correct password, you’ll be presented with a ‘Template is missing’ error. That is because we didn’t redirect you anywhere for successfully logging in or create a create.html.erb template to be rendered. Let’s fix that now.

Step 3: Helpers and a Landing Page

I’m just going to create a feeds controller and send users there on a successful login.

script/generate controller feeds index

Ok, now we have somewhere to send people when they login. What we don’t have are all the helpers to tell if someone is logged in or not. I’ve used restful authentication quite a bit so I pretty much just ganked the methods from that and tweaked them a bit for what we are doing. Put the following code in lib/google/rails/helpers.rb.

module Google
  module Rails
    module Helpers
      protected
        # Inclusion hook to make #current_user and #logged_in?
        # available as ActionView helper methods.
        def self.included(base)
          base.send :helper_method, :current_user, :logged_in?, :authorized? if base.respond_to? :helper_method
        end

        # Returns true or false if the user is logged in.
        # Preloads @current_user with the user model if they're logged in.
        def logged_in?
          !!current_user
        end

        # Accesses the current user from the session.
        # Future calls avoid the database because nil is not equal to false.
        def current_user
          @current_user ||= (login_from_session || login_from_basic_auth) unless @current_user == false
        end

        # Store the given user id in the session.
        def current_user=(new_user)
          session[:user_id] = new_user ? new_user.id : nil
          @current_user = new_user || false
        end

        # Check if the user is authorized
        #
        # Override this method in your controllers if you want to restrict access
        # to only a few actions or if you want to check if the user
        # has the correct rights.
        #
        # Example:
        #
        #  # only allow nonbobs
        #  def authorized?
        #    current_user.login != "bob" 
        #  end
        #
        def authorized?(action=nil, resource=nil, *args)
          logged_in?
        end

        # Filter method to enforce a login requirement.
        #
        # To require logins for all actions, use this in your controllers:
        #
        #   before_filter :login_required
        #
        # To require logins for specific actions, use this in your controllers:
        #
        #   before_filter :login_required, :only => [ :edit, :update ]
        #
        # To skip this in a subclassed controller:
        #
        #   skip_before_filter :login_required
        #
        def login_required
          authorized? || access_denied
        end

        # Redirect as appropriate when an access request fails.
        #
        # The default action is to redirect to the login screen.
        #
        # Override this method in your controllers if you want to have special
        # behavior in case the user is not authorized
        # to access the requested action.  For example, a popup window might
        # simply close itself.
        def access_denied
          respond_to do |format|
            format.html do
              store_location
              redirect_to login_url
            end
            # format.any doesn't work in rails version < http://dev.rubyonrails.org/changeset/8987
            # you may want to change format.any to e.g. format.any(:js, :xml)
            format.any do
              request_http_basic_authentication 'Web Password'
            end
          end
        end

        # Store the URI of the current request in the session.
        #
        # We can return to this location by calling #redirect_back_or_default.
        def store_location
          session[:return_to] = request.request_uri
        end

        # Redirect to the URI stored by the most recent store_location call or
        # to the passed default.  Set an appropriately modified
        #   after_filter :store_location, :only => [:index, :new, :show, :edit]
        # for any controller you want to be bounce-backable.
        def redirect_back_or_default(default)
          redirect_to(session[:return_to] || default)
          session[:return_to] = nil
        end

        # Called from #current_user.  First attempt to login by the user id stored in the session.
        def login_from_session
          self.current_user = User.find_by_id(session[:user_id]) if session[:user_id]
        end

        # Called from #current_user.  Now, attempt to login by basic authentication information.
        def login_from_basic_auth
          authenticate_with_http_basic do |email, password|
            self.current_user = User.authenticate(email, password)
          end
        end

        # This is ususally what you want; resetting the session willy-nilly wreaks
        # havoc with forgery protection, and is only strictly necessary on login.
        # However, **all session state variables should be unset here**.
        def logout_keeping_session!
          # Kill server-side auth cookie
          @current_user = false     # not logged in, and don't do it for me
          session[:user_id] = nil   # keeps the session but kill our variable
          # explicitly kill any other session variables you set
        end

        # The session should only be reset at the tail end of a form POST --
        # otherwise the request forgery protection fails. It's only really necessary
        # when you cross quarantine (logged-out to logged-in).
        def logout_killing_session!
          logout_keeping_session!
          reset_session
        end
    end
  end
end

Now that we have the code to do what we need, we have to include it in our application controller, which should now look something like this.

class ApplicationController < ActionController::Base
  helper :all
  protect_from_forgery
  filter_parameter_logging :password

  include Google::Rails::Helpers
end

Step 4: User Model

In order to easily relate activity in the system to a user, we’ll need a database table and model.

script/generate model user username:string

Be sure to migrate the database now.

rake db:migrate

Ok, so we aren’t going to store passwords. All we are going to store for now is the username. You can decorate this table with other information such as name and favorite puppy later, but we’ll keep it simple for now. Let’s add an authenticate method to the user model to keep our controller simple (when we update it to use the helper methods above).

class User < ActiveRecord::Base
  def self.authenticate(username, password)
    return false if username.blank? || password.blank?
    Google::Base.establish_connection(username, password)
    User.find_or_create_by_username(username)
  rescue Google::LoginError
    false
  end
end

Obviously, we will return false if username or password is blank. The next line, attempts to authenticate the user with google. If that fails, the library raises Google::LoginError so we rescue that and return false. If it doesn’t fail, it will continue to the find_or_create_by, which does exactly what it says and returns the found or created user. That means this method either returns a User instance or false, which makes it easy to use in the controller. Speaking of the controller, let’s update our sessions controller to take advantage of User#authenticate and the helpers we added a while ago.

class SessionsController < ApplicationController
  before_filter :login_required, :only => :destroy
  def new
  end

  def create
    self.current_user = User.authenticate(params[:username], params[:password])
    if logged_in?
      redirect_to feeds_url
    else
      render :action => 'new'
    end
  end

  def destroy
    logout_killing_session!
    redirect_to login_url
  end
end

Note that we are using feeds_url so you’ll need to add the following route as well.

map.resources :feeds

You can now login and you’ll see the Feeds#index file. That is great and all but we should probably allow people to logout too, so let’s create app/views/layouts/application.html.erb.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Google Authentication Example</title>
</head>

<body>
    <div id="wrapper">
        <div id="header">
            <%- if logged_in? -%>
                <p>
                    Logged in as <%=h current_user.username %>. 
                    <%= link_to 'Logout?', logout_url %>
                </p>
            <%- end -%>
        </div>
        <div id="content">
            <%= yield %>
        </div>
    </div>
</body>
</html>

The End

That is pretty much it. You have authentication to verify the person is who they say they are. You have a users table that you can store information about the user in. You don’t have to store passwords. You don’t have to provide lost password functionality. Pretty cool.

All that said, I wouldn’t use this as the only form of authentication in your app unless your target is straight up geeks. I lied about everyone having google accounts, sorry.

You can download a zip of the sample app I just created if you like.

Behind closed doors, over the past few weeks, I have been hacking away on the twitter gem. The last time I wrote about it was because I added STDIN. The updates I’ll run through right now are way cooler than that, I promise. Below are some of the highlights including a complete rewrite of the command line interface and support for the search API.

When Stuff Goes Wrong (0.2.7)

Originally the twitter gem pretty much either worked or didn’t and provided very little help as to which happened. In 0.2.7, I added a few more exceptions, RateExceeded and Unavailable, to better give you an idea of what is happening in the abyss known as the twitter gem. I also added the source parameter, so now when you command line tweet it says ‘twitter gem’ and it’s easier for those using the gem with their applications to add their source.

CLI with Multiple Account Support (0.3.0)

During a fit of frustration with Twitterrific and all other twitter clients, I completely rewrote the command line interface for the gem using Main and Highline. Other than cleaning up the command line code, this rewrite also brought a few new features.

First, the twitter gem now supports multiple accounts. It creates and maintains a sqlite database locally to store your accounts, tweets and last seen information. You can see most of the code here. If you already have a .twitter file in your home directory, it will attempt to import that account and run with it when you use a command the first time. See the rubyforge page for more installation instructions and such.

Second, anytime you check your timelines and replies, it remembers what you’ve seen. Next time you check, it will only show you the new tweets.

Third, and finally, I updated the API wrapping part of the gem to support all of twitter’s API updates. Methods like, friendship_exists?, update_location, update_delivery_device, favorites, create_favorite, destroy_favorite, block and unblock were added.

Holy Sexy Formatting Batman! (0.3.1)

In one of my faovrite updates, I spent some time formatting the tweets to optimize readability. The usernames are right aligned and the tweet text wraps neatly beside them with short simple lines. The usernames and the tweet text are now really easy to scan quickly and read. In fact, I’ve actually started using the gem as my main twitter client which previously I had not.

terminal_output.png

Each line of tweet text is about 5 words (space separated). Also, a small dashed line above and a blank line below the date helps separate one tweet from the next. You know you have too much time on your hands when…

Identi.ca Support (0.3.3)

I almost didn’t add it as I don’t really want to support something that comes up against Twitter, my favorite application, but then I thought what the heck. The initialize method now has an option key for :api_host so you can use it with Identi.ca’s API.

Twitter::Base.new('username', 'password', :api_host => 'identi.ca/api')

Search! (0.3.4)

I decided to use my brand spanking new HTTParty gem to wrap the Twitter Search API (formerly Summize). With this update you can do some nifty searching of all those tweets. I decided to use chaining when applying the search criteria and I think it turned out kind of cool. Check out a few of the examples below.

#searches all tweets for httparty
Twitter::Search.new('httparty').each { |r| puts r.inspect }

# searches all of jnunemaker's tweets for httparty
Twitter::Search.new('httparty').from('jnunemaker').each { |r| puts r.inspect }

# searches all tweets from jnunemaker to oaknd1
Twitter::Search.new.from('jnunemaker').to('oaknd1').each { |r| puts r.inspect }

# you can also use fetch to actually just get the parsed response
# if you don't feel like iterating through the objects right away
# or if you need pagination information
Twitter::Search.new.from('jnunemaker').to('oaknd1').fetch()

If you are curious as to what all can be searched, just check out the simple spec I have for the class. Heck, you can just check out the code for the search class itself which rings in at less than 100 lines thanks to the HTTPartying I was doing.

I’m really excited about the command line interface (I have a few more ideas for it) and the search code. The new gem is on rubyforge (twitter) and github (jnunemaker-twitter). Install and enjoy!

Google App Engine Hack Day

July 30th, 2008

appengine.gif

Tomorrow, myself and a few members of the South Bend ruby group are going to Google’s App Engine Hackathon in Chicago. I’ve been toying around with it a bit, forcing myself to learn something new.

If anyone reading this is going, let me know and we’ll have to meet up. I’ve got a few good posts lined up (one google related), but I don’t want to steal the thunder from HTTParty just yet, so I’ll put them on the back burner.

So I’ve made a boatload of gems that consume web services (twitter, lastfm, magnolia, delicious, google reader, google analytics). Each time I get a bit better at it and learn something new. I pretty much am only interested in consuming restful api’s and over time I’ve started to see a pattern. The other day I thought, wouldn’t it be nice if that pattern were wrapped up as a present for me (and others) to use? The answer is yes and it is named HTTParty.

The Pattern

Every web service related gem I’ve written makes requests and parses responses into ruby objects. So first let’s start with requests. The request methods that you make the most use of are get and post, with put and delete occasionally sliding in. I don’t know about you but I constantly forget how to use net/http. First, make the http, then the request. Is it get or post? How do I get the response body? Maybe I’m forgetful, but I always needed to have net/http’s rdoc open when working with it. Not anymore, though, cause now I just HTTParty like it’s 1999.

A Princely Example

To find something that I haven’t written a gem for, I hopped over to programmable web’s API directory. I chose Who is my representative’s API because, frankly, I didn’t know it existed and it was really basic. So let’s say we want to get who my rep is:

require 'rubygems'
require 'httparty'

class Representative
  include HTTParty
end

puts Representative.get('http://whoismyrepresentative.com/whoismyrep.php?zip=46544').inspect
# "<result n='1' ><rep name='Joe Donnelly' state='IN' district='2' phone='(202) 225-3915' office='1218 Longworth' link='http://donnelly.house.gov/' /></result>"

Yep, that is it. Include HTTParty and you are good to go. So, that was easy. Now we can make requests, but our response was just plain old xml. We want ruby objects! Let’s require rexml or install hpricot or libxml-ruby next, right? Wrong! Just set the format.

Automatically Parse XML

require 'rubygems'
require 'httparty'

class Representative
  include HTTParty
  format :xml
end

puts Representative.get('http://whoismyrepresentative.com/whoismyrep.php?zip=46544').inspect
# {"result"=>{"n"=>"1", "rep"=>{"name"=>"Joe Donnelly", "district"=>"2", "office"=>"1218 Longworth", "phone"=>"(202) 225-3915", "link"=>"http://donnelly.house.gov/", "state"=>"IN"}}}

Yep, I’m not joking. That works, but let’s wrap things up a little bit and make a prettier API for the developer that will eventually use our Representative library.

require 'rubygems'
require 'httparty'

class Representative
  include HTTParty
  format :xml

  def self.find_by_zip(zip)
    get('http://whoismyrepresentative.com/whoismyrep.php', :query => {:zip => zip})
  end
end

puts Representative.find_by_zip(46544).inspect
# {"result"=>{"n"=>"1", "rep"=>{"name"=>"Joe Donnelly", "district"=>"2", "office"=>"1218 Longworth", "phone"=>"(202) 225-3915", "link"=>"http://donnelly.house.gov/", "state"=>"IN"}}}

There, that is a little better. One simple module include (HTTParty) and we can make requests and automatically get our xml responses parsed. Not to mention it’s so easy my mom could do it. What? Oh, you want to see JSON. Sure, no problem.

Automatically Parse JSON

require 'rubygems'
require 'httparty'

class Representative
  include HTTParty
  format :json

  def self.find_by_zip(zip)
    get('http://whoismyrepresentative.com/whoismyrep.php', :query => {:zip => zip, :output => 'json'})
  end
end

puts Representative.find_by_zip(46544).inspect
# {"results"=>[{"name"=>"Joe Donnelly", "district"=>"2", "office"=>"1218 Longworth", "phone"=>"(202) 225-3915", "link"=>"http://donnelly.house.gov/", "state"=>"IN"}]}

Holla! You thought you had me but you didn’t. Let’s make our example a little bit more complicated and add another method to get all the reps by name.

require 'rubygems'
require 'httparty'

class Representative
  include HTTParty
  format :json

  def self.find_by_zip(zip)
    get('http://whoismyrepresentative.com/whoismyrep.php', :query => {:zip => zip, :output => 'json'})
  end

  def self.get_all_by_name(last_name)
    get('http://whoismyrepresentative.com/getall_reps_byname.php', :query => {:lastname => last_name, :output => 'json'})
  end
end

puts Representative.get_all_by_name('Donnelly').inspect
# {"results"=>[{"district"=>"2", "last"=>"Donnelly", "first"=>"Joe", "state"=>"IN", "party"=>"D"}]}

Notice any problems with that? I do. I’m repeating the domain and the output format in each request. Let’s fix that.

Helpers To DRY Things Up

require 'rubygems'
require 'httparty'

class Representative
  include HTTParty
  base_uri 'whoismyrepresentative.com'
  default_params :output => 'json'
  format :json

  def self.find_by_zip(zip)
    get('/whoismyrep.php', :query => {:zip => zip})
  end

  def self.get_all_by_name(last_name)
    get('/getall_reps_byname.php', :query => {:lastname => last_name})
  end
end

puts Representative.get_all_by_name('Donnelly').inspect
# {"results"=>[{"district"=>"2", "last"=>"Donnelly", "first"=>"Joe", "state"=>"IN", "party"=>"D"}]}

I used base_uri to remove the duplication of the domain and default_params to automatically append :output => ‘json’ to each request. The previous examples give you a really good example of what HTTParty can do, but there is one last example I’ll show.

HTTP Authentication

The only thing we haven’t covered is authentication. API keys are simple, just add them to default_params I showed in the last example, but what about http authentication? Twitter uses http authentication, so our next example will use them.

require 'rubygems'
require 'httparty'

class Twitter
  include HTTParty
  base_uri 'twitter.com'
  basic_auth 'username', 'password'
end

puts Twitter.post('/statuses/update.json', :query => {:status => "It's an HTTParty and everyone is invited!"}).inspect
# {"user"=>{"name"=>"Snitch Test", "url"=>nil, "id"=>808074, "description"=>nil, "protected"=>true, "screen_name"=>"snitch_test", "followers_count"=>1, "location"=>"Hollywood, CA", "profile_image_url"=>"http://static.twitter.com/images/default_profile_normal.png"}, "favorited"=>nil, "truncated"=>false, "text"=>"It's an HTTParty and everyone is invited!", "id"=>870885871, "in_reply_to_user_id"=>nil, "in_reply_to_status_id"=>nil, "source"=>"web", "created_at"=>"Mon Jul 28 20:07:52 +0000 2008"}

Cool. Wait, HTTP Authentication has to go in the class? No silly, Trix are for kids! The basic_auth method is just a class method so you can use it wherever class methods are acceptable. Try this on for size:

require 'rubygems'
require 'httparty'

class Twitter
  include HTTParty
  base_uri 'twitter.com'

  def initialize(user, pass)
    self.class.basic_auth user, pass
  end

  def post(text)
    self.class.post('/statuses/update.json', :query => {:status => text})
  end
end

puts Twitter.new('username', 'password').post("It's an HTTParty and everyone is invited!").inspect
# {"user"=>{"name"=>"Snitch Test", "url"=>nil, "id"=>808074, "description"=>nil, "protected"=>true, "screen_name"=>"snitch_test", "followers_count"=>1, "location"=>"Hollywood, CA", "profile_image_url"=>"http://static.twitter.com/images/default_profile_normal.png"}, "favorited"=>nil, "truncated"=>false, "text"=>"It's an HTTParty and everyone is invited!", "id"=>870885871, "in_reply_to_user_id"=>nil, "in_reply_to_status_id"=>nil, "source"=>"web", "created_at"=>"Mon Jul 28 20:07:52 +0000 2008"}

Miscellaneous

Conclusion

Ok, so that was a really long introduction, but hopefully it was helpful. I’ve also included examples in the gem for those who want to venture more (twitter, delicious, amazon associates web services, most basic usage). Also, don’t be afraid of the code as it doesn’t have much (< 140 lines at the moment).

In a certain application I’m working on, each account and each site added in that account get a unique subdomain. Last time, I talked about making sure to reserve a few subdomains for yourself. This time we’ll talk about automating a chore that comes along with developing a subdomained application.

example.com

Did you know that example.com is a reserved domain? Yep, no one can register it. This means that you can use it for development locally if you want your development addresses to feel more production-like (rather than using localhost).

The easiest way to make this work on a mac is to edit your /etc/hosts file. If you open up that file, you can point example.com to localhost by adding the following line of code to the file.

127.0.0.1 example.com

Now you can visit http://example.com:3000 in your browser and you’ll hit the app that is running locally on port 3000 (assuming you have one running).

So what does this have to do with your fancy subdomains? Well, you can separate entries by a space and put multiple example.com subdomains in /etc/hosts.

127.0.0.1    localhost example.com www.example.com foobar.example.com

Now you can visit www.example.com:3000 or foobar.example.com:3000 in your browser and it will just work. This is really handy for manually testing your great new slice of subdomainage.

The Problem

Sweet you’re thinking, so you dig in and start creating subdomained sweetness. Once you get your signup form done, you decide to test it out. Let’s see, put in my email, select my monthly plan and I’ll use orderedlist as the subdomain. You hit the ‘Signup’ button and you are redirected to orderedlist.example.com, where you promptly receive the “can’t find server” error page. What? Well, you didn’t add it to your /etc/hosts file. Remember you added www and foobar but not orderedlist.

What a pain. For an app that just uses subdomains as simple account keys, you could just add a few to your /etc/hosts file and always remember to use those when testing your app. No biggie.

What I’m working on though uses subdomains for a few things. It’s a multi-site application, which means customers will be able to point their domain at it and the app will know how exactly how to deal with that. DNS changes like this can sometimes take time to resolve. I didn’t want a customer waiting for their DNS to go through to use the application, so each site also has a special subdomain based on the account that will work right away until DNS propagates. This means they can get in and work on their sites before flipping the DNS switch, if the site already exists on the interwebs. Also a sidebar, it will make https for the administration area possible, but that is a sidebar.

The Solution

So I was getting a little tired of editing the /etc/hosts file and decided to automate the process a bit with a rake task. Here is the task.

namespace :harmony do
  desc "Adds the necessary hosts to your /etc/hosts file" 
  task :hosts => :environment do
    default, hosts = %w[www.example.com test.example.com], []

    # add all the site temporary domains
    hosts << Site.find(:all).inject([]) do |collection, site|
      collection << site.harmony_url
    end

    # add all the account subdomains
    hosts << Account.find(:all).inject([]) do |collection, account|
      collection << account.harmony_url
    end

    harmony_host_line = "127.0.0.1 " + hosts.flatten.sort.unshift(default).join(' ')
    lines = rio('/etc/hosts').readlines.reject { |l| l =~ /www\.example\.com\stest\.example\.com/ } << harmony_host_line
    rio('/tmp/hostscopy') < lines
    %x[sudo -p "Password:" cp /tmp/hostscopy /etc/hosts]
  end
end

Now when I run rake harmony:hosts, my host file is updated from live data in the application. I’ll walk you through the details.

First, I setup the defaults that I always want to be used no matter what data is in the database. Then I loop through all the items that have subdomains (sites and accounts), building up the hosts array.

default, hosts = %w[www.example.com test.example.com], []

# add all the site temporary domains
hosts << Site.find(:all).inject([]) do |collection, site|
  collection << site.harmony_url
end

# add all the account subdomains
hosts << Account.find(:all).inject([]) do |collection, account|
  collection << account.harmony_url
end

Once all that is done, I build the line that needs to be put into my hosts file.

harmony_host_line = "127.0.0.1 " + hosts.flatten.sort.unshift(default).join(' ')

Now that I have the line I need in my hosts file, I get all the lines in my current host file, rejecting the one that starts with my defaults, as I want to replace that line with the line created above.

lines = rio('/etc/hosts').readlines.reject { |l| l =~ /www\.example\.com\stest\.example\.com/ } << harmony_host_line

The code above uses a library called rio. It makes manipulating files and such as easy as fooling around with strings and arrays. Now that I have all the lines that will make up my new /etc/hosts file, I put them in a tmp file and then copy that over my current file forcing a sudo along the way.

rio('/tmp/hostscopy') < lines
%x[sudo -p "Password:" cp /tmp/hostscopy /etc/hosts]

So that is pretty much it. This becomes really handy when I pair it with loading my fixtures. I know, I know, fixtures suck. In this instance, I’m actually not minding them. Below is the rake task to load my fixtures into my development database (I’m using rSpec thus the spec namespace) and then run the hosts task to refresh my /etc/hosts.

namespace :harmony do
  desc "Does all the stuff to make it so you can run harmony." 
  task :refresh => :environment do
    %w[spec:db:fixtures:load harmony:hosts].each do |task|
      Rake::Task[task].invoke
    end
  end
end

Now I can run rake harmony:refresh and my database is chock full of sample data and my /etc/hosts file is up to date based on that data.

A new person to the project could now git clone, rake db:migrate, rake harmony:refresh and have a nice setup of the app to browse around in.