rack-cache-buster 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +34 -0
- data/lib/rack/cache_buster.rb +54 -0
- data/lib/rack/cache_buster/auto.rb +13 -0
- data/test/auto_test.rb +70 -0
- data/test/cache_busting_test.rb +47 -0
- data/test/test_helper.rb +15 -0
- metadata +61 -0
data/README.markdown
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Rack::CacheBuster
|
2
|
+
|
3
|
+
Place this in your rack stack and all caching will be gone.
|
4
|
+
|
5
|
+
Add an optional key to salt the ETags in and out of your app.
|
6
|
+
|
7
|
+
## Usage:
|
8
|
+
use Rack::CacheBuster, APP_VERSION
|
9
|
+
|
10
|
+
# Rack::CacheBuster::Auto
|
11
|
+
|
12
|
+
Use to wind down a running app when needed.
|
13
|
+
|
14
|
+
Busts the cache when enabled file exists, salts the ETags when key_file exists.
|
15
|
+
|
16
|
+
## Usage:
|
17
|
+
|
18
|
+
### Setup:
|
19
|
+
|
20
|
+
ensure you have a REVISION file on your production servers, capistrano does this by default.
|
21
|
+
|
22
|
+
use Rack::CacheBuster::Auto, File.join(Rails.root, "REVISION"), File.join(Rails.root, "WIND_DOWN")
|
23
|
+
|
24
|
+
### Before you deploy:
|
25
|
+
|
26
|
+
* Find the maximum cache duration for all pages in your app, start this process with at least that amount time before you deploy.
|
27
|
+
|
28
|
+
* Create a WIND_DOWN file in the app root of all your app servers.
|
29
|
+
|
30
|
+
* Restart all instances (touch tmp/restart.txt will be fine for passenger apps).
|
31
|
+
|
32
|
+
During this period all caching will be disabled. If you respect If-Modified (using `stale?` in rails for example), these will still be respected.
|
33
|
+
|
34
|
+
Once all the caches should have expired you can deploy as normal.
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class CacheBuster
|
5
|
+
autoload :Auto, "rack/cache_buster/auto"
|
6
|
+
|
7
|
+
def initialize(app, key = nil)
|
8
|
+
@app = app
|
9
|
+
if key
|
10
|
+
@key = "-"+Digest::MD5.hexdigest(key).freeze
|
11
|
+
@key_regexp = /#{@key}/.freeze
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
env = env.dup
|
17
|
+
unpatch_etag!(env) if key
|
18
|
+
|
19
|
+
status, headers, body = app.call(env)
|
20
|
+
|
21
|
+
headers = headers.dup
|
22
|
+
patch_etag!(headers) if key
|
23
|
+
bust_cache!(headers)
|
24
|
+
|
25
|
+
[status, headers, body]
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
QUOTE_STRIPPER=/^"|"$/.freeze
|
30
|
+
ETAGGY_HEADERS = ["HTTP_IF_NONE_MATCH", "HTTP_IF_MATCH", "HTTP_IF_RANGE"].freeze
|
31
|
+
ETag = "ETag".freeze
|
32
|
+
CacheControl = "Cache-Control".freeze
|
33
|
+
|
34
|
+
attr_reader :app
|
35
|
+
attr_reader :key
|
36
|
+
|
37
|
+
|
38
|
+
def bust_cache!(headers)
|
39
|
+
headers[CacheControl] = "no-cache"
|
40
|
+
end
|
41
|
+
|
42
|
+
def unpatch_etag!(headers)
|
43
|
+
ETAGGY_HEADERS.each do |k|
|
44
|
+
headers[k] = headers[k].gsub(@key_regexp, "") if headers[k]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def patch_etag!(headers)
|
49
|
+
stripped_tag = headers[ETag].to_s.gsub(QUOTE_STRIPPER, "")
|
50
|
+
return if stripped_tag.empty?
|
51
|
+
headers[ETag] = %Q{"#{stripped_tag}#{key}"}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rack/cache_buster"
|
2
|
+
|
3
|
+
class Rack::CacheBuster::Auto < Rack::CacheBuster
|
4
|
+
def initialize(app, key_filename, enabled_filename)
|
5
|
+
@enabled = File.exists?(enabled_filename)
|
6
|
+
key = File.exists?(key_filename) && File.open(key_filename).gets.chomp
|
7
|
+
super(app, key)
|
8
|
+
end
|
9
|
+
|
10
|
+
def bust_cache!(headers)
|
11
|
+
@enabled ? super(headers) : headers
|
12
|
+
end
|
13
|
+
end
|
data/test/auto_test.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
#FOO
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class CacheBusterAutoTest < IntegrationTest
|
5
|
+
DIGEST_OF_HASH_FOO=Digest::MD5.hexdigest("#FOO")
|
6
|
+
|
7
|
+
context "with no key files" do
|
8
|
+
setup do
|
9
|
+
@app = Rack::CacheBuster::Auto.new(CACHEY_APP, "/no-file-here.txt", "/no-file-here.txt")
|
10
|
+
end
|
11
|
+
|
12
|
+
should "not set cache-control to no-cache" do
|
13
|
+
get "/foooo"
|
14
|
+
assert_not_equal "no-cache", last_response["Cache-Control"]
|
15
|
+
end
|
16
|
+
|
17
|
+
should "leave the ETag alone" do
|
18
|
+
get "/foooo"
|
19
|
+
assert_equal %Q{"an-etag"}, last_response["ETag"]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "with a key file" do
|
24
|
+
setup do
|
25
|
+
@app = Rack::CacheBuster::Auto.new(CACHEY_APP, __FILE__, "/no-file-here.txt")
|
26
|
+
end
|
27
|
+
|
28
|
+
should "still not set cache-control to no-cache" do
|
29
|
+
get "/foooo"
|
30
|
+
assert_not_equal "no-cache", last_response["Cache-Control"]
|
31
|
+
end
|
32
|
+
|
33
|
+
should "fiddle with the ETag" do
|
34
|
+
get "/foooo"
|
35
|
+
assert_equal %Q{"an-etag-#{DIGEST_OF_HASH_FOO}"}, last_response["ETag"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "with an enabled file" do
|
40
|
+
setup do
|
41
|
+
@app = Rack::CacheBuster::Auto.new(CACHEY_APP, "/no-file-here.txt", __FILE__)
|
42
|
+
end
|
43
|
+
|
44
|
+
should "set cache-control to no-cache" do
|
45
|
+
get "/foooo"
|
46
|
+
assert_equal "no-cache", last_response["Cache-Control"]
|
47
|
+
end
|
48
|
+
|
49
|
+
should "leave the ETag alone" do
|
50
|
+
get "/foooo"
|
51
|
+
assert_equal %Q{"an-etag"}, last_response["ETag"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "with an enabled file and a key file" do
|
56
|
+
setup do
|
57
|
+
@app = Rack::CacheBuster::Auto.new(CACHEY_APP, __FILE__, __FILE__)
|
58
|
+
end
|
59
|
+
|
60
|
+
should "set cache-control to no-cache" do
|
61
|
+
get "/foooo"
|
62
|
+
assert_equal "no-cache", last_response["Cache-Control"]
|
63
|
+
end
|
64
|
+
|
65
|
+
should "fiddle with the ETag" do
|
66
|
+
get "/foooo"
|
67
|
+
assert_equal %Q{"an-etag-#{DIGEST_OF_HASH_FOO}"}, last_response["ETag"]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class CacheBustingTest < IntegrationTest
|
4
|
+
DIGEST_OF_FOO=Digest::MD5.hexdigest("foo")
|
5
|
+
|
6
|
+
should "forward the request to the app" do
|
7
|
+
given_env = nil
|
8
|
+
@app = Rack::CacheBuster.new(lambda { |env| given_env = env; [200, {}, []] })
|
9
|
+
get "/foooo"
|
10
|
+
assert_not_nil given_env
|
11
|
+
assert_equal given_env["PATH_INFO"], "/foooo"
|
12
|
+
end
|
13
|
+
|
14
|
+
should "forward the request to the app with digests removed" do
|
15
|
+
given_env = nil
|
16
|
+
|
17
|
+
@app = Rack::CacheBuster.new(lambda { |env| given_env = env; [200, {}, []] }, "foo")
|
18
|
+
|
19
|
+
["If-None-Match", "If-Match", "If-Range"].each do |k|
|
20
|
+
header(k, "foo-#{DIGEST_OF_FOO}")
|
21
|
+
end
|
22
|
+
|
23
|
+
get "/foooo"
|
24
|
+
|
25
|
+
Rack::CacheBuster::ETAGGY_HEADERS.each do |k|
|
26
|
+
assert_equal "foo", given_env[k], "#{k} should be set to foo"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
should "set cache-control to no-cache" do
|
31
|
+
@app = Rack::CacheBuster.new(CACHEY_APP)
|
32
|
+
get "/foooo"
|
33
|
+
assert_equal "no-cache", last_response["Cache-Control"]
|
34
|
+
end
|
35
|
+
|
36
|
+
should 'append version to etag of response' do
|
37
|
+
@app = Rack::CacheBuster.new(CACHEY_APP, "foo")
|
38
|
+
get "/"
|
39
|
+
assert_equal %Q{"an-etag-#{DIGEST_OF_FOO}"}, last_response["ETag"]
|
40
|
+
end
|
41
|
+
|
42
|
+
should "leave the ETag alone if no key is given" do
|
43
|
+
@app = Rack::CacheBuster.new(CACHEY_APP)
|
44
|
+
get "/foooo"
|
45
|
+
assert_equal %Q{"an-etag"}, last_response["ETag"]
|
46
|
+
end
|
47
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$:<<File.join(File.dirname(__FILE__), *%w[.. lib])
|
2
|
+
require 'shoulda'
|
3
|
+
require 'rack/cache_buster'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'rack/test'
|
6
|
+
|
7
|
+
class IntegrationTest < Test::Unit::TestCase
|
8
|
+
include Rack::Test::Methods
|
9
|
+
attr_reader :app
|
10
|
+
|
11
|
+
CACHEY_APP = lambda{|env| [200, {"Cache-Control" => "max-age=200, public", "ETag" => '"an-etag"'}, []] }
|
12
|
+
|
13
|
+
def test_nothing
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-cache-buster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tom Lea
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-21 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: commit@tomlea.co.uk
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- README.markdown
|
26
|
+
- test/auto_test.rb
|
27
|
+
- test/cache_busting_test.rb
|
28
|
+
- test/test_helper.rb
|
29
|
+
- lib/rack/cache_buster/auto.rb
|
30
|
+
- lib/rack/cache_buster.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://tomlea.co.uk/
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options:
|
37
|
+
- --main
|
38
|
+
- README.markdown
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project: rack-cache-buster
|
56
|
+
rubygems_version: 1.3.5
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Place this in your rack stack and all caching will be gone.
|
60
|
+
test_files: []
|
61
|
+
|