travis-web/lib/travis/web/app.rb
2012-12-12 23:39:03 +01:00

143 lines
3.5 KiB
Ruby

require 'rack'
require 'rack/ssl'
require 'rack/cache'
require 'rack/protection'
require 'delegate'
require 'time'
class Travis::Web::App
autoload :MobileRedirect, 'travis/web/app/mobile_redirect'
# Simple Rack router that behaves like a hash.
# Key is the path, value the response.
class Router < DelegateClass(Hash)
def initialize
super({})
end
def call(env)
self[env['PATH_INFO']]
end
end
def self.new(options = {})
return super unless options[:environment] == 'development'
proc { |e| super.call(e) } # poor man's reloader
end
attr_reader :app, :router, :environment, :version, :last_modified, :age, :options, :root
def initialize(options = {})
@options = options
@environment = options.fetch(:environment)
@root = options.fetch(:root)
@router = Router.new
@app = builder.to_app
@version = File.read File.expand_path('version', root)
@last_modified = Time.now
@age = 60 * 60 * 24 * 365
load_routes
end
def call(env)
app.call(env)
end
private
def load_routes
each_file { |f| router[route_for(f)] = response_for(f) }
router.default = router['/']
end
def response_for(file)
content = File.read(file)
set_config(content) if config_needed? file
headers = {
'Content-Length' => content.bytesize.to_s,
'Content-Location' => route_for(file),
'Cache-Control' => cache_control(file),
'Content-Location' => route_for(file),
'Content-Type' => mime_type(file),
'ETag' => version,
'Last-Modified' => last_modified.httpdate,
'Expires' => (last_modified + age).httpdate,
'Vary' => vary_for(file)
}
[ 200, headers, [ content ] ]
end
def each_file
pattern = File.join(root, '**/*')
Dir.glob(pattern) { |f| yield f if File.file? f }
end
def prefix?(file)
file =~ /^(styles|scripts)\//
end
def config_needed?(file)
index?(file) || file.end_with?('spec.html')
end
def index?(file)
file.end_with? 'index.html'
end
def route_for(file)
file = file.sub("#{root}/", '')
file = File.join(version, file) if prefix? file
file = "" if index? file
"/#{file}"
end
def cache_control(file)
case route_for(file)
when '/' then "public, must-revalidate"
when '/version' then "no-cache"
else "public, max-age=#{age}"
end
end
def vary_for(file)
case route_for(file)
when '/' then 'Accept'
when '/version' then '*'
else ''
end
end
def mime_type(file)
Rack::Mime.mime_type File.extname(file)
end
def set_config(string)
string.gsub! %r(<meta (rel|name)="travis\.([^"]*)" (href|value)="([^"]*)"[^>]*>) do
%(<meta #{$1}="travis.#{$2}" #{$3}="#{options[$2.to_sym] || $4}">)
end
string.gsub! %r{(src|href)="(\/?)((styles|scripts)\/[^"]*)"} do
%(#$1="#$2#{version}/#$3")
end
end
def builder
builder = Rack::Builder.new
if environment == 'production'
builder.use Rack::SSL
builder.use Rack::Cache
end
builder.use Rack::Deflater
builder.use Rack::Head
builder.use Rack::Protection::XSSHeader
builder.use Rack::Protection::FrameOptions
builder.use Rack::Protection::PathTraversal
builder.use Rack::ConditionalGet
builder.use MobileRedirect
builder.run router
builder
end
end