Skip to content

Commit

Permalink
Adds experimental PageCache
Browse files Browse the repository at this point in the history
  • Loading branch information
radiospiel committed May 9, 2012
1 parent 2b6e451 commit 2dc7f3a
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 0 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
source :rubygems

gem "sinatra-synchrony"
gem "mime-types"
gem "rmagick"
gem "thin"
gem "sinatra-contrib", :group => :development
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ GEM
linecache19 (0.5.12)
ruby_core_source (>= 0.1.4)
metaclass (0.0.1)
mime-types (1.18)
mocha (0.11.0)
metaclass (~> 0.0.1)
multi_json (1.2.0)
Expand Down Expand Up @@ -82,6 +83,7 @@ PLATFORMS

DEPENDENCIES
image_size
mime-types
mocha
psych
rack-test
Expand Down
6 changes: 6 additions & 0 deletions app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
# would break the parameter parsing.
set :protection, :except => :path_traversal

if settings.static
STDERR.puts "Enable ResponseCache in #{settings.public_folder}"
require "#{File.dirname(__FILE__)}/lib/page_cache"
use PageCache, settings.public_folder
end

require "#{File.dirname(__FILE__)}/lib/http"
require "#{File.dirname(__FILE__)}/lib/robot"
require "#{File.dirname(__FILE__)}/lib/assembly_line"
Expand Down
73 changes: 73 additions & 0 deletions lib/page_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require 'fileutils'

# PageCache is a Rack middleware that caches responses for successful GET requests
# to disk. This works similar to Rails' page caching, allowing you to cache dynamic
# pages to static files that can be served directly by a front end webserver.
class PageCache
# Initialize a new ReponseCache object with the given arguments. Arguments:
# * app : The next middleware in the chain. This is always called.
# * cache : The place to cache responses. If a string is provided, a disk
# cache is used, and all cached files will use this directory as the root directory.
# If anything other than a string is provided, it should respond to []=, which will
# be called with a path string and a body value (the 3rd element of the response).
# * &block : If provided, it is called with the environment and the response from the next middleware.
# It should return nil or false if the path should not be cached, and should return
# the pathname to use as a string if the result should be cached.
# If not provided, the DEFAULT_PATH_PROC is used.
def initialize(app, cache, &block)
@app = app
@cache = cache.sub(/\/*$/, "/") # normalize path to have a single trailing slash.
end

def call(env)
res = @app.call(env)
page_cache(env, res)
res
end

def page_cache(env, res)
# If the request was successful (response status 200), was a GET request, and
# had an empty query string, this request is probably cacheable.
return res unless env['REQUEST_METHOD'] == 'GET' and res[0] == 200 and env['QUERY_STRING'] == ""

# But the content_type, that can be derived from the request path - and is what
# the web server would use in the next request - must match the content type
# in the response.
return unless matching_content_type?(env, res)

# Build path for cache file. The path must not contain '..', to prevent directory
# traversal attacks.
path = env['PATH_INFO']
return if path.include?('..')

# We cannot use File.join here, because this would eat the double slashes in the path_info.
# The @cache instance variable, however, is normalized to end in a single slash.
path = "#{@cache}/#{path}"

# If the cache file exists already, then we run inside rackup.
# The cached file will be delivered by Rack::Static, and we don't need
# (and don't want to: this zeroes the file, for some reason) to rewrite the path
return if File.exists?(path)
puts "Cache in #{path}"

FileUtils.mkdir_p(File.dirname(path))
File.open(path, 'wb') do |f|
res[2].each do |c|
puts "Write #{c.length} byte"
f.write(c)
end
end
end

def matching_content_type?(env, res)
return unless content_type = res[1]['Content-Type']

request_path = env['REQUEST_PATH']
mime_types = MIME::Types.type_for(request_path)
mime_types = [ "text/html" ] if mime_types.empty?

mime_types.any? do |mime_type|
content_type.index(mime_type)
end
end
end
Empty file added public/.empty_dir
Empty file.

0 comments on commit 2dc7f3a

Please sign in to comment.