Serving maintenance page with Rack Middleware

Posted by Dan Sosedoff on April 09, 2011

Its not that important to show message while doing a regular app update, which takes seconds to complete, but when you need to perform some maintenance tasks (tuning, configuration, installation) its better to give users information when its going to be finished. All big websites have their own fancy maintenance page, but for small projects it does not matter.

Here is a Rack::Maintenance module that provides such functionality:

module Rack
  class Maintenance
    File = ::File
 
    def initialize(app, dir=nil, &block)
      @app = app
      @block = block
      @dir = dir || Dir.pwd
      @file = File.join(File.expand_path(@dir), '.maintenance')
    end
 
    def call(env)
      if File.exists?(@file)
        body = @block.nil? ? default_prompt(time_info) : @block.call(time_info)
        res = Response.new
        res.write(body)
        res.finish
      else
        @app.call(env)
      end
    end
 
    private
 
    def time_info
      t_since, t_until = [nil, nil]
      File.open(@file) do |f|
        t_since = f.mtime
        t_until = Time.parse(f.gets) rescue nil
        t_until = nil if t_until.kind_of?(Time) && t_until < Time.now
      end
      {:since => t_since, :until => t_until}
    end
 
    def default_prompt(t)
      if t[:until].nil? # no idea when do we end
        msg = "We're doing stuff on our servers and will be back shortly.<br/><br/>"
        msg << "<small>started: #{t[:since]}</small>"
      else
        msg = "We'll be back online on <b>#{t[:until]}</b>"
      end
      "<center><br/><br/><h1>Maintenance.</h1><p>#{msg}</p></center>"
    end
  end
end

Usage

By default middleware will look into current working directory for file with filename “.maintenance”.
Request will be passed next if no file found.

Simpliest example:

use Rack::Maintenance

Set custom directory:

use Rack::Maintenance, '/path/to/dir'

Customize default maintenance prompt:

use Rack::Maintenance do
  "We're doing some stuff. Come back later."
end

If you want to use timestamps in your prompt just put a valid time string
into your .maintenance file:

File.open('.maintenance', 'w') { |f| f.write(Time.now + 3600) }

and then use it in your prompt:

use Rack::Maintenance do |t|
  "We're doing some stuff since #{t[:since} will be done at #{t[:until]}."
end

Results

Screen shot 2011-04-09 at 8.57.40 PM


Screen shot 2011-04-09 at 9.09.46 PM

Restrict access to all mobile and bot clients with Rack Middleware

Posted by Dan Sosedoff on April 09, 2011

Rack middleware is a convenient way to process request before it gets to the application level. Of course, a lot of tasks might be performed within the app, but its not even necessary to do so. Since every ruby web framework is based on rack it makes sense to handle it with middleware.

Here is a small module to bounce off all non-desktop clients (mobile devices, crawlers):

module Rack
  class Restrict
    REGEX_MOBILE = /(blackberry|motorokr|motorola|sony|windows ce|240x320|176x220|palm|mobile|iphone|ipod|symbian|nokia|samsung|midp)/i
    REGEX_BOTS = /(google|yahoo|baidu|bot|webalta|ia_archiver)/
 
    def initialize(app)
      @app = app
    end
 
    def call(env)
      str = env['HTTP_USER_AGENT']
      restrict = !str.match(REGEX_MOBILE).nil? || !str.match(REGEX_BOTS).nil?
      restrict ? forbidden! : @app.call(env)
    end
 
    def forbidden!
      [403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, []]
    end
  end
end

Usage: (sample config.ru):

require 'rack'
require 'rack/contrib'
require 'app.rb'
 
use Rack::Restrict
run Sinatra::Application