rack-cache 0.3.0 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-cache might be problematic. Click here for more details.
- data/CHANGES +43 -0
- data/README +18 -9
- data/Rakefile +1 -14
- data/TODO +13 -14
- data/doc/configuration.markdown +7 -153
- data/doc/faq.markdown +8 -0
- data/doc/index.markdown +7 -9
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +5 -11
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +190 -52
- data/lib/rack/cache/entitystore.rb +10 -4
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +52 -16
- data/lib/rack/cache/options.rb +60 -39
- data/lib/rack/cache/request.rb +11 -15
- data/lib/rack/cache/response.rb +221 -30
- data/lib/rack/cache/storage.rb +1 -2
- data/rack-cache.gemspec +9 -15
- data/test/cache_test.rb +9 -6
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +251 -169
- data/test/entitystore_test.rb +12 -11
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +57 -14
- data/test/options_test.rb +11 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +164 -23
- data/test/spec_setup.rb +7 -0
- metadata +12 -20
- data/doc/events.dot +0 -27
- data/lib/rack/cache/config.rb +0 -65
- data/lib/rack/cache/config/busters.rb +0 -16
- data/lib/rack/cache/config/default.rb +0 -133
- data/lib/rack/cache/config/no-cache.rb +0 -13
- data/lib/rack/cache/core.rb +0 -299
- data/lib/rack/cache/headers.rb +0 -325
- data/lib/rack/utils/environment_headers.rb +0 -78
- data/test/config_test.rb +0 -66
- data/test/core_test.rb +0 -84
- data/test/environment_headers_test.rb +0 -69
- data/test/headers_test.rb +0 -298
- data/test/logging_test.rb +0 -45
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'digest/sha1'
|
2
2
|
|
3
3
|
module Rack::Cache
|
4
|
+
|
4
5
|
# Entity stores are used to cache response bodies across requests. All
|
5
6
|
# Implementations are required to calculate a SHA checksum of the data written
|
6
7
|
# which becomes the response body's key.
|
@@ -13,7 +14,7 @@ module Rack::Cache
|
|
13
14
|
def slurp(body)
|
14
15
|
digest, size = Digest::SHA1.new, 0
|
15
16
|
body.each do |part|
|
16
|
-
size += part
|
17
|
+
size += bytesize(part)
|
17
18
|
digest << part
|
18
19
|
yield part
|
19
20
|
end
|
@@ -21,7 +22,13 @@ module Rack::Cache
|
|
21
22
|
[ digest.hexdigest, size ]
|
22
23
|
end
|
23
24
|
|
24
|
-
|
25
|
+
if ''.respond_to?(:bytesize)
|
26
|
+
def bytesize(string); string.bytesize; end
|
27
|
+
else
|
28
|
+
def bytesize(string); string.size; end
|
29
|
+
end
|
30
|
+
|
31
|
+
private :slurp, :bytesize
|
25
32
|
|
26
33
|
|
27
34
|
# Stores entity bodies on the heap using a Hash object.
|
@@ -90,7 +97,7 @@ module Rack::Cache
|
|
90
97
|
end
|
91
98
|
|
92
99
|
def read(key)
|
93
|
-
File.
|
100
|
+
File.open(body_path(key), 'rb') { |f| f.read }
|
94
101
|
rescue Errno::ENOENT
|
95
102
|
nil
|
96
103
|
end
|
@@ -241,7 +248,6 @@ module Rack::Cache
|
|
241
248
|
|
242
249
|
MEMCACHE = MemCache
|
243
250
|
MEMCACHED = MemCache
|
244
|
-
|
245
251
|
end
|
246
252
|
|
247
253
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Rack::Cache
|
4
|
+
class Key
|
5
|
+
include Rack::Utils
|
6
|
+
|
7
|
+
# Implement .call, since it seems like the "Rack-y" thing to do. Plus, it
|
8
|
+
# opens the door for cache key generators to just be blocks.
|
9
|
+
def self.call(request)
|
10
|
+
new(request).generate
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(request)
|
14
|
+
@request = request
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generate a normalized cache key for the request.
|
18
|
+
def generate
|
19
|
+
parts = []
|
20
|
+
parts << @request.scheme << "://"
|
21
|
+
parts << @request.host
|
22
|
+
|
23
|
+
if @request.scheme == "https" && @request.port != 443 ||
|
24
|
+
@request.scheme == "http" && @request.port != 80
|
25
|
+
parts << ":" << @request.port.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
parts << @request.script_name
|
29
|
+
parts << @request.path_info
|
30
|
+
|
31
|
+
if qs = query_string
|
32
|
+
parts << "?"
|
33
|
+
parts << qs
|
34
|
+
end
|
35
|
+
|
36
|
+
parts.join
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
# Build a normalized query string by alphabetizing all keys/values
|
41
|
+
# and applying consistent escaping.
|
42
|
+
def query_string
|
43
|
+
return nil if @request.query_string.nil?
|
44
|
+
|
45
|
+
@request.query_string.split(/[&;] */n).
|
46
|
+
map { |p| unescape(p).split('=', 2) }.
|
47
|
+
sort.
|
48
|
+
map { |k,v| "#{escape(k)}=#{escape(v)}" }.
|
49
|
+
join('&')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/rack/cache/metastore.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require 'rack'
|
2
1
|
require 'fileutils'
|
3
2
|
require 'digest/sha1'
|
3
|
+
require 'rack/utils'
|
4
4
|
|
5
5
|
module Rack::Cache
|
6
6
|
|
@@ -25,7 +25,8 @@ module Rack::Cache
|
|
25
25
|
# Rack::Cache::Response object if the cache hits or nil if no cache entry
|
26
26
|
# was found.
|
27
27
|
def lookup(request, entity_store)
|
28
|
-
|
28
|
+
key = cache_key(request)
|
29
|
+
entries = read(key)
|
29
30
|
|
30
31
|
# bail out if we have nothing cached
|
31
32
|
return nil if entries.empty?
|
@@ -37,9 +38,7 @@ module Rack::Cache
|
|
37
38
|
|
38
39
|
req, res = match
|
39
40
|
if body = entity_store.open(res['X-Content-Digest'])
|
40
|
-
|
41
|
-
response.activate!
|
42
|
-
response
|
41
|
+
restore_response(res, body)
|
43
42
|
else
|
44
43
|
# TODO the metastore referenced an entity that doesn't exist in
|
45
44
|
# the entitystore. we definitely want to return nil but we should
|
@@ -50,21 +49,17 @@ module Rack::Cache
|
|
50
49
|
# Write a cache entry to the store under the given key. Existing
|
51
50
|
# entries are read and any that match the response are removed.
|
52
51
|
# This method calls #write with the new list of cache entries.
|
53
|
-
#--
|
54
|
-
# TODO canonicalize URL key
|
55
52
|
def store(request, response, entity_store)
|
56
|
-
key = request
|
53
|
+
key = cache_key(request)
|
57
54
|
stored_env = persist_request(request)
|
58
55
|
|
59
56
|
# write the response body to the entity store if this is the
|
60
57
|
# original response.
|
61
|
-
response['X-
|
62
|
-
if response['X-Content-Digest'].nil?
|
58
|
+
if response.headers['X-Content-Digest'].nil?
|
63
59
|
digest, size = entity_store.write(response.body)
|
64
|
-
response['X-Content-Digest'] = digest
|
65
|
-
response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
|
60
|
+
response.headers['X-Content-Digest'] = digest
|
61
|
+
response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
|
66
62
|
response.body = entity_store.open(digest)
|
67
|
-
response.activate!
|
68
63
|
end
|
69
64
|
|
70
65
|
# read existing cache entries, remove non-varying, and add this one to
|
@@ -75,11 +70,41 @@ module Rack::Cache
|
|
75
70
|
(vary == res['Vary']) &&
|
76
71
|
requests_match?(vary, env, stored_env)
|
77
72
|
end
|
78
|
-
|
73
|
+
|
74
|
+
headers = persist_response(response)
|
75
|
+
headers.delete 'Age'
|
76
|
+
|
77
|
+
entries.unshift [stored_env, headers]
|
79
78
|
write key, entries
|
79
|
+
key
|
80
|
+
end
|
81
|
+
|
82
|
+
# Generate a cache key for the request.
|
83
|
+
def cache_key(request)
|
84
|
+
keygen = request.env['rack-cache.cache_key'] || Key
|
85
|
+
keygen.call(request)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Invalidate all cache entries that match the request.
|
89
|
+
def invalidate(request, entity_store)
|
90
|
+
modified = false
|
91
|
+
key = cache_key(request)
|
92
|
+
entries =
|
93
|
+
read(key).map do |req, res|
|
94
|
+
response = restore_response(res)
|
95
|
+
if response.fresh?
|
96
|
+
response.expire!
|
97
|
+
modified = true
|
98
|
+
[req, persist_response(response)]
|
99
|
+
else
|
100
|
+
[req, res]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
write key, entries if modified
|
80
104
|
end
|
81
105
|
|
82
106
|
private
|
107
|
+
|
83
108
|
# Extract the environment Hash from +request+ while making any
|
84
109
|
# necessary modifications in preparation for persistence. The Hash
|
85
110
|
# returned must be marshalable.
|
@@ -89,6 +114,19 @@ module Rack::Cache
|
|
89
114
|
env
|
90
115
|
end
|
91
116
|
|
117
|
+
# Converts a stored response hash into a Response object. The caller
|
118
|
+
# is responsible for loading and passing the body if needed.
|
119
|
+
def restore_response(hash, body=nil)
|
120
|
+
status = hash.delete('X-Status').to_i
|
121
|
+
Rack::Cache::Response.new(status, hash, body)
|
122
|
+
end
|
123
|
+
|
124
|
+
def persist_response(response)
|
125
|
+
hash = response.headers.to_hash
|
126
|
+
hash['X-Status'] = response.status.to_s
|
127
|
+
hash
|
128
|
+
end
|
129
|
+
|
92
130
|
# Determine whether the two environment hashes are non-varying based on
|
93
131
|
# the vary response header value provided.
|
94
132
|
def requests_match?(vary, env1, env2)
|
@@ -122,7 +160,6 @@ module Rack::Cache
|
|
122
160
|
end
|
123
161
|
|
124
162
|
private
|
125
|
-
|
126
163
|
# Generate a SHA1 hex digest for the specified string. This is a
|
127
164
|
# simple utility method for meta store implementations.
|
128
165
|
def hexdigest(data)
|
@@ -130,7 +167,6 @@ module Rack::Cache
|
|
130
167
|
end
|
131
168
|
|
132
169
|
public
|
133
|
-
|
134
170
|
# Concrete MetaStore implementation that uses a simple Hash to store
|
135
171
|
# request/response pairs on the heap.
|
136
172
|
class Heap < MetaStore
|
data/lib/rack/cache/options.rb
CHANGED
@@ -1,26 +1,28 @@
|
|
1
|
-
require 'rack'
|
1
|
+
require 'rack/cache/key'
|
2
2
|
require 'rack/cache/storage'
|
3
3
|
|
4
4
|
module Rack::Cache
|
5
|
+
|
5
6
|
# Configuration options and utility methods for option access. Rack::Cache
|
6
7
|
# uses the Rack Environment to store option values. All options documented
|
7
8
|
# below are stored in the Rack Environment as "rack-cache.<option>", where
|
8
9
|
# <option> is the option name.
|
9
|
-
#
|
10
|
-
# The #set method can be used within an event or a top-level configuration
|
11
|
-
# block to configure a option values. When #set is called at the top-level,
|
12
|
-
# the value applies to all requests; when called from within an event, the
|
13
|
-
# values applies only to the request being processed.
|
14
|
-
|
15
10
|
module Options
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
11
|
+
def self.option_accessor(key)
|
12
|
+
name = option_name(key)
|
13
|
+
define_method(key) { || options[name] }
|
14
|
+
define_method("#{key}=") { |value| options[name] = value }
|
15
|
+
define_method("#{key}?") { || !! options[name] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def option_name(key)
|
19
|
+
case key
|
20
|
+
when Symbol ; "rack-cache.#{key}"
|
21
|
+
when String ; key
|
22
|
+
else raise ArgumentError
|
22
23
|
end
|
23
24
|
end
|
25
|
+
module_function :option_name
|
24
26
|
|
25
27
|
# Enable verbose trace logging. This option is currently enabled by
|
26
28
|
# default but is likely to be disabled in a future release.
|
@@ -44,9 +46,22 @@ module Rack::Cache
|
|
44
46
|
# recommended.
|
45
47
|
option_accessor :metastore
|
46
48
|
|
47
|
-
# A
|
48
|
-
#
|
49
|
-
#
|
49
|
+
# A custom cache key generator, which can be anything that responds to :call.
|
50
|
+
# By default, this is the Rack::Cache::Key class, but you can implement your
|
51
|
+
# own generator. A cache key generator gets passed a request and generates the
|
52
|
+
# appropriate cache key.
|
53
|
+
#
|
54
|
+
# In addition to setting the generator to an object, you can just pass a block
|
55
|
+
# instead, which will act as the cache key generator:
|
56
|
+
#
|
57
|
+
# set :cache_key do |request|
|
58
|
+
# request.fullpath.replace(/\//, '-')
|
59
|
+
# end
|
60
|
+
option_accessor :cache_key
|
61
|
+
|
62
|
+
# A URI specifying the entity-store implementation that should be used to
|
63
|
+
# store response bodies. See the metastore option for information on
|
64
|
+
# supported URI schemes.
|
50
65
|
#
|
51
66
|
# If no entity store is specified the 'heap:/' store is assumed. This
|
52
67
|
# implementation has significant draw-backs so explicit configuration is
|
@@ -70,6 +85,16 @@ module Rack::Cache
|
|
70
85
|
# Default: ['Authorization', 'Cookie']
|
71
86
|
option_accessor :private_headers
|
72
87
|
|
88
|
+
# Specifies whether the client can force a cache reload by including a
|
89
|
+
# Cache-Control "no-cache" directive in the request. This is enabled by
|
90
|
+
# default for compliance with RFC 2616.
|
91
|
+
option_accessor :allow_reload
|
92
|
+
|
93
|
+
# Specifies whether the client can force a cache revalidate by including
|
94
|
+
# a Cache-Control "max-age=0" directive in the request. This is enabled by
|
95
|
+
# default for compliance with RFC 2616.
|
96
|
+
option_accessor :allow_revalidate
|
97
|
+
|
73
98
|
# The underlying options Hash. During initialization (or outside of a
|
74
99
|
# request), this is a default values Hash. During a request, this is the
|
75
100
|
# Rack environment Hash. The default values Hash is merged in underneath
|
@@ -88,8 +113,10 @@ module Rack::Cache
|
|
88
113
|
# exactly as specified. The +option+ argument may also be a Hash in
|
89
114
|
# which case each key/value pair is merged into the environment as if
|
90
115
|
# the #set method were called on each.
|
91
|
-
def set(option, value=self)
|
92
|
-
if
|
116
|
+
def set(option, value=self, &block)
|
117
|
+
if block_given?
|
118
|
+
write_option option, block
|
119
|
+
elsif value == self
|
93
120
|
self.options = option.to_hash
|
94
121
|
else
|
95
122
|
write_option option, value
|
@@ -97,6 +124,21 @@ module Rack::Cache
|
|
97
124
|
end
|
98
125
|
|
99
126
|
private
|
127
|
+
def initialize_options(options={})
|
128
|
+
@default_options = {
|
129
|
+
'rack-cache.cache_key' => Key,
|
130
|
+
'rack-cache.verbose' => true,
|
131
|
+
'rack-cache.storage' => Rack::Cache::Storage.instance,
|
132
|
+
'rack-cache.metastore' => 'heap:/',
|
133
|
+
'rack-cache.entitystore' => 'heap:/',
|
134
|
+
'rack-cache.default_ttl' => 0,
|
135
|
+
'rack-cache.private_headers' => ['Authorization', 'Cookie'],
|
136
|
+
'rack-cache.allow_reload' => true,
|
137
|
+
'rack-cache.allow_revalidate' => true
|
138
|
+
}
|
139
|
+
self.options = options
|
140
|
+
end
|
141
|
+
|
100
142
|
def read_option(key)
|
101
143
|
options[option_name(key)]
|
102
144
|
end
|
@@ -104,26 +146,5 @@ module Rack::Cache
|
|
104
146
|
def write_option(key, value)
|
105
147
|
options[option_name(key)] = value
|
106
148
|
end
|
107
|
-
|
108
|
-
def option_name(key)
|
109
|
-
case key
|
110
|
-
when Symbol ; "rack-cache.#{key}"
|
111
|
-
when String ; key
|
112
|
-
else raise ArgumentError
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
private
|
117
|
-
def initialize_options(options={})
|
118
|
-
@default_options = {
|
119
|
-
'rack-cache.verbose' => true,
|
120
|
-
'rack-cache.storage' => Rack::Cache::Storage.instance,
|
121
|
-
'rack-cache.metastore' => 'heap:/',
|
122
|
-
'rack-cache.entitystore' => 'heap:/',
|
123
|
-
'rack-cache.default_ttl' => 0,
|
124
|
-
'rack-cache.private_headers' => ['Authorization', 'Cookie']
|
125
|
-
}
|
126
|
-
self.options = options
|
127
|
-
end
|
128
149
|
end
|
129
150
|
end
|
data/lib/rack/cache/request.rb
CHANGED
@@ -1,19 +1,15 @@
|
|
1
1
|
require 'rack/request'
|
2
|
-
require 'rack/cache/
|
3
|
-
require 'rack/utils/environment_headers'
|
2
|
+
require 'rack/cache/cachecontrol'
|
4
3
|
|
5
4
|
module Rack::Cache
|
5
|
+
|
6
6
|
# Provides access to the HTTP request. The +request+ and +original_request+
|
7
7
|
# objects exposed by the Core caching engine are instances of this class.
|
8
8
|
#
|
9
9
|
# Request objects respond to a variety of convenience methods, including
|
10
10
|
# everything defined by Rack::Request as well as the Headers and
|
11
11
|
# RequestHeaders modules.
|
12
|
-
|
13
12
|
class Request < Rack::Request
|
14
|
-
include Rack::Cache::Headers
|
15
|
-
include Rack::Cache::RequestHeaders
|
16
|
-
|
17
13
|
# The HTTP request method. This is the standard implementation of this
|
18
14
|
# method but is respecified here due to libraries that attempt to modify
|
19
15
|
# the behavior to respect POST tunnel method specifiers. We always want
|
@@ -22,16 +18,16 @@ module Rack::Cache
|
|
22
18
|
@env['REQUEST_METHOD']
|
23
19
|
end
|
24
20
|
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
# ...
|
29
|
-
# end
|
30
|
-
def request_method?(*methods)
|
31
|
-
method = request_method
|
32
|
-
methods.any? { |test| test.to_s.upcase == method }
|
21
|
+
# A CacheControl instance based on the request's Cache-Control header.
|
22
|
+
def cache_control
|
23
|
+
@cache_control ||= CacheControl.new(env['HTTP_CACHE_CONTROL'])
|
33
24
|
end
|
34
25
|
|
35
|
-
|
26
|
+
# True when the Cache-Control/no-cache directive is present or the
|
27
|
+
# Pragma header is set to no-cache.
|
28
|
+
def no_cache?
|
29
|
+
cache_control['no-cache'] ||
|
30
|
+
env['HTTP_PRAGMA'] == 'no-cache'
|
31
|
+
end
|
36
32
|
end
|
37
33
|
end
|
data/lib/rack/cache/response.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
require 'time'
|
1
2
|
require 'set'
|
2
|
-
require 'rack/
|
3
|
+
require 'rack/response'
|
4
|
+
require 'rack/utils'
|
5
|
+
require 'rack/cache/cachecontrol'
|
3
6
|
|
4
7
|
module Rack::Cache
|
8
|
+
|
5
9
|
# Provides access to the response generated by the downstream application. The
|
6
10
|
# +response+, +original_response+, and +entry+ objects exposed by the Core
|
7
11
|
# caching engine are instances of this class.
|
@@ -14,21 +18,14 @@ module Rack::Cache
|
|
14
18
|
# not perform many of the same initialization and finalization tasks. For
|
15
19
|
# example, the body is not slurped during initialization and there are no
|
16
20
|
# facilities for generating response output.
|
17
|
-
|
18
21
|
class Response
|
19
22
|
include Rack::Response::Helpers
|
20
|
-
include Rack::Cache::Headers
|
21
|
-
include Rack::Cache::ResponseHeaders
|
22
23
|
|
23
|
-
#
|
24
|
-
attr_accessor :status
|
24
|
+
# Rack response tuple accessors.
|
25
|
+
attr_accessor :status, :headers, :body
|
25
26
|
|
26
|
-
# The
|
27
|
-
|
28
|
-
attr_accessor :body
|
29
|
-
|
30
|
-
# The response headers.
|
31
|
-
attr_reader :headers
|
27
|
+
# The time when the Response object was instantiated.
|
28
|
+
attr_reader :now
|
32
29
|
|
33
30
|
# Create a Response instance given the response status code, header hash,
|
34
31
|
# and body.
|
@@ -37,7 +34,7 @@ module Rack::Cache
|
|
37
34
|
@headers = Rack::Utils::HeaderHash.new(headers)
|
38
35
|
@body = body
|
39
36
|
@now = Time.now
|
40
|
-
@headers['Date'] ||= now.httpdate
|
37
|
+
@headers['Date'] ||= @now.httpdate
|
41
38
|
end
|
42
39
|
|
43
40
|
def initialize_copy(other)
|
@@ -45,32 +42,226 @@ module Rack::Cache
|
|
45
42
|
@headers = other.headers.dup
|
46
43
|
end
|
47
44
|
|
48
|
-
# Return the
|
49
|
-
def
|
50
|
-
headers
|
45
|
+
# Return the status, headers, and body in a three-tuple.
|
46
|
+
def to_a
|
47
|
+
[status, headers.to_hash, body]
|
51
48
|
end
|
52
49
|
|
53
|
-
#
|
54
|
-
|
55
|
-
|
50
|
+
# Status codes of responses that MAY be stored by a cache or used in reply
|
51
|
+
# to a subsequent request.
|
52
|
+
#
|
53
|
+
# http://tools.ietf.org/html/rfc2616#section-13.4
|
54
|
+
CACHEABLE_RESPONSE_CODES = [
|
55
|
+
200, # OK
|
56
|
+
203, # Non-Authoritative Information
|
57
|
+
300, # Multiple Choices
|
58
|
+
301, # Moved Permanently
|
59
|
+
302, # Found
|
60
|
+
404, # Not Found
|
61
|
+
410 # Gone
|
62
|
+
].to_set
|
63
|
+
|
64
|
+
# A Hash of name=value pairs that correspond to the Cache-Control header.
|
65
|
+
# Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
|
66
|
+
# of true. This method always returns a Hash, empty if no Cache-Control
|
67
|
+
# header is present.
|
68
|
+
def cache_control
|
69
|
+
@cache_control ||= CacheControl.new(headers['Cache-Control'])
|
56
70
|
end
|
57
71
|
|
58
|
-
#
|
59
|
-
|
60
|
-
|
72
|
+
# Set the Cache-Control header to the values specified by the Hash. See
|
73
|
+
# the #cache_control method for information on expected Hash structure.
|
74
|
+
def cache_control=(value)
|
75
|
+
if value.respond_to? :to_hash
|
76
|
+
cache_control.clear
|
77
|
+
cache_control.merge!(value)
|
78
|
+
value = cache_control.to_s
|
79
|
+
end
|
80
|
+
|
81
|
+
if value.nil? || value.empty?
|
82
|
+
headers.delete('Cache-Control')
|
83
|
+
else
|
84
|
+
headers['Cache-Control'] = value
|
85
|
+
end
|
61
86
|
end
|
62
87
|
|
63
|
-
#
|
64
|
-
|
65
|
-
|
88
|
+
# Determine if the response is "fresh". Fresh responses may be served from
|
89
|
+
# cache without any interaction with the origin. A response is considered
|
90
|
+
# fresh when it includes a Cache-Control/max-age indicator or Expiration
|
91
|
+
# header and the calculated age is less than the freshness lifetime.
|
92
|
+
def fresh?
|
93
|
+
ttl && ttl > 0
|
66
94
|
end
|
67
95
|
|
68
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
96
|
+
# Determine if the response is worth caching under any circumstance. Responses
|
97
|
+
# marked "private" with an explicit Cache-Control directive are considered
|
98
|
+
# uncacheable
|
99
|
+
#
|
100
|
+
# Responses with neither a freshness lifetime (Expires, max-age) nor cache
|
101
|
+
# validator (Last-Modified, ETag) are considered uncacheable.
|
102
|
+
def cacheable?
|
103
|
+
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
|
104
|
+
return false if cache_control.no_store? || cache_control.private?
|
105
|
+
validateable? || fresh?
|
72
106
|
end
|
73
107
|
|
74
|
-
|
108
|
+
# Determine if the response includes headers that can be used to validate
|
109
|
+
# the response with the origin using a conditional GET request.
|
110
|
+
def validateable?
|
111
|
+
headers.key?('Last-Modified') || headers.key?('ETag')
|
112
|
+
end
|
113
|
+
|
114
|
+
# Mark the response "private", making it ineligible for serving other
|
115
|
+
# clients.
|
116
|
+
def private=(value)
|
117
|
+
value = value ? true : nil
|
118
|
+
self.cache_control = cache_control.
|
119
|
+
merge('public' => !value, 'private' => value)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Indicates that the cache must not serve a stale response in any
|
123
|
+
# circumstance without first revalidating with the origin. When present,
|
124
|
+
# the TTL of the response should not be overriden to be greater than the
|
125
|
+
# value provided by the origin.
|
126
|
+
def must_revalidate?
|
127
|
+
cache_control.must_revalidate || cache_control.proxy_revalidate
|
128
|
+
end
|
129
|
+
|
130
|
+
# Mark the response stale by setting the Age header to be equal to the
|
131
|
+
# maximum age of the response.
|
132
|
+
def expire!
|
133
|
+
headers['Age'] = max_age.to_s if fresh?
|
134
|
+
end
|
135
|
+
|
136
|
+
# The date, as specified by the Date header. When no Date header is present,
|
137
|
+
# set the Date header to Time.now and return.
|
138
|
+
def date
|
139
|
+
if date = headers['Date']
|
140
|
+
Time.httpdate(date)
|
141
|
+
else
|
142
|
+
headers['Date'] = now.httpdate unless headers.frozen?
|
143
|
+
now
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# The age of the response.
|
148
|
+
def age
|
149
|
+
(headers['Age'] || [(now - date).to_i, 0].max).to_i
|
150
|
+
end
|
151
|
+
|
152
|
+
# The number of seconds after the time specified in the response's Date
|
153
|
+
# header when the the response should no longer be considered fresh. First
|
154
|
+
# check for a s-maxage directive, then a max-age directive, and then fall
|
155
|
+
# back on an expires header; return nil when no maximum age can be
|
156
|
+
# established.
|
157
|
+
def max_age
|
158
|
+
cache_control.shared_max_age ||
|
159
|
+
cache_control.max_age ||
|
160
|
+
(expires && (expires - date))
|
161
|
+
end
|
162
|
+
|
163
|
+
# The value of the Expires header as a Time object.
|
164
|
+
def expires
|
165
|
+
headers['Expires'] && Time.httpdate(headers['Expires'])
|
166
|
+
end
|
75
167
|
|
168
|
+
# The number of seconds after which the response should no longer
|
169
|
+
# be considered fresh. Sets the Cache-Control max-age directive.
|
170
|
+
def max_age=(value)
|
171
|
+
self.cache_control = cache_control.merge('max-age' => value.to_s)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Like #max_age= but sets the s-maxage directive, which applies only
|
175
|
+
# to shared caches.
|
176
|
+
def shared_max_age=(value)
|
177
|
+
self.cache_control = cache_control.merge('s-maxage' => value.to_s)
|
178
|
+
end
|
179
|
+
|
180
|
+
# The response's time-to-live in seconds, or nil when no freshness
|
181
|
+
# information is present in the response. When the responses #ttl
|
182
|
+
# is <= 0, the response may not be served from cache without first
|
183
|
+
# revalidating with the origin.
|
184
|
+
def ttl
|
185
|
+
max_age - age if max_age
|
186
|
+
end
|
187
|
+
|
188
|
+
# Set the response's time-to-live for shared caches to the specified number
|
189
|
+
# of seconds. This adjusts the Cache-Control/s-maxage directive.
|
190
|
+
def ttl=(seconds)
|
191
|
+
self.shared_max_age = age + seconds
|
192
|
+
end
|
193
|
+
|
194
|
+
# Set the response's time-to-live for private/client caches. This adjusts
|
195
|
+
# the Cache-Control/max-age directive.
|
196
|
+
def client_ttl=(seconds)
|
197
|
+
self.max_age = age + seconds
|
198
|
+
end
|
199
|
+
|
200
|
+
# The String value of the Last-Modified header exactly as it appears
|
201
|
+
# in the response (i.e., no date parsing / conversion is performed).
|
202
|
+
def last_modified
|
203
|
+
headers['Last-Modified']
|
204
|
+
end
|
205
|
+
|
206
|
+
# The literal value of ETag HTTP header or nil if no ETag is specified.
|
207
|
+
def etag
|
208
|
+
headers['ETag']
|
209
|
+
end
|
210
|
+
|
211
|
+
# Determine if the response was last modified at the time provided.
|
212
|
+
# time_value is the exact string provided in an origin response's
|
213
|
+
# Last-Modified header.
|
214
|
+
def last_modified_at?(time_value)
|
215
|
+
time_value && last_modified == time_value
|
216
|
+
end
|
217
|
+
|
218
|
+
# Determine if response's ETag matches the etag value provided. Return
|
219
|
+
# false when either value is nil.
|
220
|
+
def etag_matches?(etag)
|
221
|
+
etag && self.etag == etag
|
222
|
+
end
|
223
|
+
|
224
|
+
# Headers that MUST NOT be included with 304 Not Modified responses.
|
225
|
+
#
|
226
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
227
|
+
NOT_MODIFIED_OMIT_HEADERS = %w[
|
228
|
+
Allow
|
229
|
+
Content-Encoding
|
230
|
+
Content-Language
|
231
|
+
Content-Length
|
232
|
+
Content-MD5
|
233
|
+
Content-Type
|
234
|
+
Last-Modified
|
235
|
+
].to_set
|
236
|
+
|
237
|
+
# Modify the response so that it conforms to the rules defined for
|
238
|
+
# '304 Not Modified'. This sets the status, removes the body, and
|
239
|
+
# discards any headers that MUST NOT be included in 304 responses.
|
240
|
+
#
|
241
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
242
|
+
def not_modified!
|
243
|
+
self.status = 304
|
244
|
+
self.body = []
|
245
|
+
NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
# The literal value of the Vary header, or nil when no header is present.
|
250
|
+
def vary
|
251
|
+
headers['Vary']
|
252
|
+
end
|
253
|
+
|
254
|
+
# Does the response include a Vary header?
|
255
|
+
def vary?
|
256
|
+
! vary.nil?
|
257
|
+
end
|
258
|
+
|
259
|
+
# An array of header names given in the Vary header or an empty
|
260
|
+
# array when no Vary header is present.
|
261
|
+
def vary_header_names
|
262
|
+
return [] unless vary = headers['Vary']
|
263
|
+
vary.split(/[\s,]+/)
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
76
267
|
end
|