rack-cache-buster 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|