rack snippets

How to install thin, rack and eventmachine on Windows/Cygwin

Tagged rack, eventmachine, windows, cygwin, thin  Languages bash

There's no official Windows binary release of eventmachine for the latest version of thin (1.0.0). Thin needs at least version (0.12.2) of eventmachine, so you'll have to use the latest GitHub code (currently 0.12.3) to install thin on Windblows and CygWin:

git clone git://github.com/eventmachine/eventmachine.git
cd eventmachine
rake gem
gem install pkg/eventmachine-0.12.3.gem
gem install thin rack

How to setup and use Rack::Cache with Rails

Tagged rack, rack::cache, cache, rails  Languages ruby

Add Rack::Cache to your Gemfile:

gem 'rack-cache', :require => 'rack/cache'

Configure Rails to use Rack::Cache by adding this to config/environment.rb:

config.middleware.use Rack::Cache,
  :verbose => true,
  :metastore   => 'file:/var/cache/rack/meta',
  :entitystore => 'file:/var/cache/rack/body'

This will cache content in /var/cache/rack/meta. Check the documentation for instructions on how to use memcached.

Verify configuration

Verify the configuration:

rake middleware

You should see something like this:

…
use Rack::Cache, {:metastore=>"file:/var/cache/rack/meta", :entitystore=>"file:/var/cache/rack/body", :verbose=>true}
…

Tell Rack::Cache to cache your content

Tell Rack to cache data by calling expires_in from your controller's action or from a Rails filter:

expires_in 5.minutes, :public => true

Rails sets cache-control to private by default, Rack::Cache needs public content.

Don't cache private data

Note that you must be careful not to cache private data when a user is signed in. In this case you should set the Cache-Control header to private or completely avoid using expires_in:

fail "Don't cache private data" if signed_in?
expires_in 5.minutes, :public => true

Verify caching

With verbose set to true, you'll see this in the logs:

[cache] trace: cache miss
[cache] trace: fetching response from backend
[cache] trace: store backend response in cache (ttl: 300s)
[cache] trace: storing response in cache
[cache] trace: delivering response ...
[cache] trace: cache hit (ttl: 276s)
[cache] trace: delivering response ...

Caching more than one type of content

Make sure you cache e.g. HTML and JSON separately. You could use the Vary HTTP header to achieve this:

response.headers['Vary'] = 'Accept'

The problem is that almost every browser version has a unique Accept header.

Instead you could try using the cache_key configuration option to generate a cache key per content type:

:cache_key  =>  lambda { |request|
  Rack::Cache::Key.new(request).generate + ":jebus"
}

You could also add the format request parameter to the URL, e.g. /json-and-html?format=html

Advanced configuration

In production you probably want to disallow reloading of content. This can be done with the following configuration options:

config.middleware.use Rack::Cache,
  :metastore   => 'file:/var/cache/rack/meta',
  :entitystore => 'file:/var/cache/rack/body',
  :allow_reload     => false,
  :allow_revalidate => false,

Read the source (options.rb) for more advanced configuration options (ignore_headers, private_headers, etc).

References

HTTP RFC - Cache-Control Rack::Cache options documentation Rails expires_in documentation HTTP Vary Header

Testing sessions with Sinatra and Rack::Test

Tagged session, test, sinatra, rack, cookie, rack::test, mocha  Languages ruby

You need support for testing sessions when, for example, testing authentication. The only way I've managed to get sessions to work with Sinatra and Rack::Test is by going through the whole stack, in other words calling the authentication controller as shown here:

@user = Factory(:user) # create a dummy user
User.expects(:authenticate).with(any_parameters).returns(@user) # make authenticate return the dummy user
post "/sign-in", {:email => @user.email, :password => @user.password} # login to the application and set a session variable.

After this the session populated and we're logged in when accessing the application again:

post "/articles", {:title => 'Sessions suck', :body => '...'}

On a side note, there are two ways of specifying sessions that used to work, but which no longer work with Sinatra 1.0:

get '/', :env => { :session => {:abc => 'adf'} }
get '/', {}, :session => {:abc => 'adf'}
get '/', {}, "rack.session" => {:abc => 'adf'}

The session is always empty.

There are many discussions about sessions and Rack::Test, but not one of them has a solution that works for me: * rack.session variable is missing env under test environment * Sessions with rspec and Rack.Test

Rails Memcached Status Plugin

Tagged status, rails, memcached, rack, middleware  Languages ruby

Use this Rack Middleware to check the status of memcached:

require 'socket'

class MemcachedStatus
  def initialize(app, uri)
    @app = app
    @uri = uri
  end

  def call(env)
    if env['PATH_INFO'] == @uri
      status 
    else
      @app.call(env)
    end
  end
  
  def status
    socket = TCPSocket.open('localhost', '11211')
    socket.send("stats\r\n", 0)

    statistics = []
    loop do
      data = socket.recv(4096)
      if !data || data.length == 0
        break
      end
      statistics << data
      if statistics.join.split(/\n/)[-1] =~ /END/
        break
      end
    end

    out = "
<html>
<body>
<pre>
#{statistics.join}
</pre>
</body>
</html>
"
    [200, {"Content-Type" => "text/html"}, [out]]
  ensure
    socket.close
  end
end

Add the following to config/application.rb:

config.middleware.use "MemcachedStatus", "/memcached_status"

Access status from http://localhost/memcached_status

Socket code from http://barkingiguana.com/2009/03/04/memcache-statistics-from-the-command-line/

Rack middleware for generating thumbnails on-the-fly

Tagged rack, image, thumbnail  Languages ruby
# Rack middleware for creating image thumbnails on the fly when needed. 
# Depends on ImageMagick and https://github.com/christianhellsten/thumbnail for thumbnail creation. 
#
# Use case 1: Allow any image under /images to be resized to any size
#
#   # e.g. /images/me-1280x1024.png
#   use Rack::Thumbnail, :uri => '/images'
#
# Use case 2: Allow any image under /images to be resized to 50x50 or 150x150
#
#   # e.g. /images/me-150x150.png
#   use Rack::Thumbnail, :uri => '/images', :dimensions => ['50x50', '150x150']
#
# Use case 3: Allow any image under /images to be resized to 50x50 or 150x150. Allow padding or cutting of thumbnails to fit given dimensions.
#
#   # e.g. /images/me-50x50-cut.png
#   use Rack::Thumbnail, :uri => '/images', :dimensions => ['50x50', '150x150'], :methods => { :pad => :pad_to_fit, :cut => :cut_to_fit }
#
require 'thumbnail'
class Rack::Thumbnail
  def initialize(app, options = {})
    @app = app
    @uri = options.fetch(:uri, '/images')
    @public_dir = options.fetch(:public_dir, 'public')
    @uri_regex = options.fetch(:uri_regex, /-(\d+)x(\d+)-?(\w+)?/) # Extracts width, height, method
    @allowed_methods = options.fetch(:methods, {})
    @allowed_dimensions = options[:dimensions]
  end

  def allowed_dimension?(width, height)
    @allowed_dimensions ? @allowed_dimensions.include?([width, 'x', height].join) : true
  end

  def call(env)
    path = env["PATH_INFO"]
    destination = File.join('public', path)
    if path.match(@uri) && !File.exist?(destination)
      _, width, height, method = path.match(@uri_regex).to_a
      if allowed_dimension?(width, height)
        source = File.join(@public_dir, path.gsub(@uri_regex, ''))
        Thumbnail.create(
          :in => source,
          :out => destination,
          :method => @allowed_methods.fetch((method || '').to_sym, :pad_to_fit),
          :width => width.to_i,
          :height => height.to_i
        )
      end
    end
    status, headers, response = @app.call(env)
    [status, headers, response]
  end
end

In Sinatra, generate thumbnail URLs with this helper:

helpers do
  # Generates thumbnail URLs.
  #
  # Example: 
  #  
  #  img uri=thumbnail('/images/me.png', :width => 50, :height => 50)
  #
  # uri - URI of source image.
  # options - hash containing width and height of thumbnail.
  #
  def thumbnail(uri, options = {})
    width = options.fetch(:width, 50)
    height = options.fetch(:height, 50)
    method = options[:method]
    ext = File.extname(uri)
    base = File.basename(uri, ext)
    base_uri = File.dirname(uri)
    unless method
      "#{dir}/#{base}-#{width}x#{height}#{ext}"
    else
      "#{dir}/#{base}-#{width}x#{height}-#{method}#{ext}"
    end
  end
end

World's Smallest Screenshot Server?

Tagged rack, screenshots, ruby  Languages ruby
require 'base64'
require 'selenium-webdriver'

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

  def screenshot(url, width, height, file)
    begin
      driver = Selenium::WebDriver.for :firefox
      driver.manage.window.resize_to(width, height)
      driver.navigate.to url
      driver.save_screenshot("screenshots/#{file}")
      file
    ensure
      driver.quit
    end
  end

  def call(env)
    req = Rack::Request.new(env)
    width = req.params.fetch('width') { 1024 }
    height = req.params.fetch('height') { 768 }
    url = req.params.fetch('url')
    file = Base64.urlsafe_encode64(req.params.inspect) + ".png"
    screenshot(url, width, height, file)
    env['PATH_INFO'] = "/#{file}"
    @app.call(env)
  end
end

use ScreenshotMiddleware
run Rack::URLMap.new  "/" => Rack::Directory.new('screenshots')

Also see How to capture screenshots with Selenium, Ruby and Firefox.

Screenshot server API

Barebone Web Development With Rack

Tagged rack, ruby, dump, headers  Languages ruby

Create a file named config.ru:

run lambda { |env|
  [200, {"Content-Type" => "text/html"}, [view(env)]]
}

def view(env)
  res = ""
  res << "<html><body><pre>"
  env.sort.each do |key, value|
    res << "#{key}: #{value}"
    res << "\n"
  end
  res << "</pre></body></html>"
  res
end

Start the server with e.g. rackup, puma, or thin:

$ rackup

Access http://localhost:9292 to see the request headers.