Skip to content

Instantly share code, notes, and snippets.

@tomlea
Created October 26, 2009 18:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tomlea/218919 to your computer and use it in GitHub Desktop.
Save tomlea/218919 to your computer and use it in GitHub Desktop.
# Full code is at http://github.com/cwninja/rack-cache-buster
require 'digest/md5'
module Rack
class CacheBuster
def initialize(app, key, target_time = nil)
@app, @target_time = app, target_time
@key = "-"+Digest::MD5.hexdigest(key || "blank-key").freeze
@key_regexp = /#{@key}/.freeze
end
def call(env)
status, headers, body = app.call(unpatch_etag(env))
[status, patch_etag(limit_cache(headers)), body]
end
protected
QUOTE_STRIPPER=/^"|"$/.freeze
ETAGGY_HEADERS = ["HTTP_IF_NONE_MATCH", "HTTP_IF_MATCH", "HTTP_IF_RANGE"].freeze
ETag = "ETag".freeze
attr_reader :app
attr_reader :key
def limit_cache(headers)
return headers if @target_time.nil?
cache_control_header = CacheControlHeader.new(headers)
if cache_control_header.expired?
headers
else
cache_control_header.expire_time = [@target_time, cache_control_header.expire_time].min
headers.merge(CacheControlHeader::CacheControl => cache_control_header.to_s)
end
end
def unpatch_etag(headers)
ETAGGY_HEADERS.inject(headers){|memo, k|
memo.has_key?(k) ? memo.merge(k => strip_etag(memo[k])) : memo
}
end
def strip_etag(s)
s.gsub(@key_regexp, "")
end
def modify_etag(s)
s = s.to_s.gsub(QUOTE_STRIPPER, "")
s.empty? ? s : %Q{"#{s}#{key}"}
end
def patch_etag(headers)
headers.merge(ETag => modify_etag(headers[ETag]))
end
autoload :CacheControlHeader, "rack/cache_buster/cache_control_header"
autoload :Rails, "rack/cache_buster/rails"
end
end
require "rack/cache_buster"
class Rack::CacheBuster::CacheControlHeader
CacheControl = "Cache-Control".freeze
Age = "Age".freeze
MaxAge = "max-age".freeze
def initialize(env)
@age = env[Age].to_i
@max_age = 0
if env[CacheControl]
parts = env[CacheControl].split(/ *, */)
settings, options = parts.partition{|part| part =~ /=/ }
settings = settings.inject({}){|acc, part|
k, v = part.split(/ *= */, 2)
acc.merge(k => v)
}
@max_age = settings.delete(MaxAge).to_i
@other_parts = options + settings.map{|k,v| "#{k}=#{v}"}
end
end
def expires_in
@max_age - @age
end
def expired?
expires_in <= 0
end
def expire_time
Time.now + expires_in
end
def expire_time=(t)
@max_age = [t.to_i - Time.now.to_i + @age, @age].max
end
def update_env(env)
env[CacheControl] = to_s
end
def to_s
["max-age=#{@max_age}", *@other_parts].join(", ")
end
end
require "rack/cache_buster"
class Rack::CacheBuster::Rails < Rack::CacheBuster
def initialize(app)
app_version = File.read(File.join(::Rails.root, "REVISION")) rescue nil
wind_down_time = Time.parse(File.read(File.join(::Rails.root, "WIND_DOWN"))) rescue nil
super(app, app_version, wind_down_time)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment