rack-cache 0.2.0 → 0.3.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.
Potentially problematic release.
This version of rack-cache might be problematic. Click here for more details.
- data/CHANGES +58 -0
- data/README +14 -9
- data/Rakefile +11 -5
- data/TODO +11 -19
- data/doc/configuration.markdown +9 -1
- data/doc/index.markdown +17 -10
- data/doc/layout.html.erb +1 -0
- data/doc/server.ru +34 -0
- data/lib/rack/cache/config/default.rb +1 -2
- data/lib/rack/cache/core.rb +37 -9
- data/lib/rack/cache/entitystore.rb +29 -6
- data/lib/rack/cache/headers.rb +118 -30
- data/lib/rack/cache/metastore.rb +22 -39
- data/lib/rack/cache/options.rb +11 -1
- data/lib/rack/cache/response.rb +2 -2
- data/rack-cache.gemspec +5 -4
- data/test/cache_test.rb +3 -3
- data/test/context_test.rb +255 -75
- data/test/core_test.rb +8 -8
- data/test/entitystore_test.rb +23 -11
- data/test/environment_headers_test.rb +10 -12
- data/test/headers_test.rb +106 -23
- data/test/metastore_test.rb +28 -18
- data/test/options_test.rb +13 -10
- data/test/spec_setup.rb +13 -7
- metadata +8 -4
@@ -58,6 +58,12 @@ module Rack::Cache
|
|
58
58
|
[key, size]
|
59
59
|
end
|
60
60
|
|
61
|
+
# Remove the body corresponding to key; return nil.
|
62
|
+
def purge(key)
|
63
|
+
@hash.delete(key)
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
61
67
|
def self.resolve(uri)
|
62
68
|
new
|
63
69
|
end
|
@@ -89,16 +95,19 @@ module Rack::Cache
|
|
89
95
|
nil
|
90
96
|
end
|
91
97
|
|
92
|
-
|
93
|
-
|
94
|
-
def open(key)
|
95
|
-
io = File.open(body_path(key), 'rb')
|
96
|
-
def io.each
|
98
|
+
class Body < ::File #:nodoc:
|
99
|
+
def each
|
97
100
|
while part = read(8192)
|
98
101
|
yield part
|
99
102
|
end
|
100
103
|
end
|
101
|
-
|
104
|
+
alias_method :to_path, :path
|
105
|
+
end
|
106
|
+
|
107
|
+
# Open the entity body and return an IO object. The IO object's
|
108
|
+
# each method is overridden to read 8K chunks instead of lines.
|
109
|
+
def open(key)
|
110
|
+
Body.open(body_path(key), 'rb')
|
102
111
|
rescue Errno::ENOENT
|
103
112
|
nil
|
104
113
|
end
|
@@ -121,6 +130,13 @@ module Rack::Cache
|
|
121
130
|
[key, size]
|
122
131
|
end
|
123
132
|
|
133
|
+
def purge(key)
|
134
|
+
File.unlink body_path(key)
|
135
|
+
nil
|
136
|
+
rescue Errno::ENOENT
|
137
|
+
nil
|
138
|
+
end
|
139
|
+
|
124
140
|
protected
|
125
141
|
def storage_path(stem)
|
126
142
|
File.join root, stem
|
@@ -190,6 +206,13 @@ module Rack::Cache
|
|
190
206
|
[key, size]
|
191
207
|
end
|
192
208
|
|
209
|
+
def purge(key)
|
210
|
+
cache.delete(key)
|
211
|
+
nil
|
212
|
+
rescue Memcached::NotFound
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
|
193
216
|
extend Rack::Utils
|
194
217
|
|
195
218
|
# Create MemCache store for the given URI. The URI must specify
|
data/lib/rack/cache/headers.rb
CHANGED
@@ -21,7 +21,7 @@ module Rack::Cache
|
|
21
21
|
# header is present.
|
22
22
|
def cache_control
|
23
23
|
@cache_control ||=
|
24
|
-
|
24
|
+
headers['Cache-Control'].to_s.split(/\s*[,;]\s*/).inject({}) {|hash,token|
|
25
25
|
name, value = token.split(/\s*=\s*/, 2)
|
26
26
|
hash[name.downcase] = (value || true) unless name.empty?
|
27
27
|
hash
|
@@ -79,9 +79,19 @@ module Rack::Cache
|
|
79
79
|
module ResponseHeaders
|
80
80
|
include Rack::Cache::Headers
|
81
81
|
|
82
|
-
#
|
83
|
-
#
|
84
|
-
|
82
|
+
# Status codes of responses that MAY be stored by a cache or used in reply
|
83
|
+
# to a subsequent request.
|
84
|
+
#
|
85
|
+
# http://tools.ietf.org/html/rfc2616#section-13.4
|
86
|
+
CACHEABLE_RESPONSE_CODES = [
|
87
|
+
200, # OK
|
88
|
+
203, # Non-Authoritative Information
|
89
|
+
300, # Multiple Choices
|
90
|
+
301, # Moved Permanently
|
91
|
+
302, # Found
|
92
|
+
404, # Not Found
|
93
|
+
410 # Gone
|
94
|
+
].to_set
|
85
95
|
|
86
96
|
# Determine if the response is "fresh". Fresh responses may be served from
|
87
97
|
# cache without any interaction with the origin. A response is considered
|
@@ -97,16 +107,15 @@ module Rack::Cache
|
|
97
107
|
!fresh?
|
98
108
|
end
|
99
109
|
|
100
|
-
# Determine if the response is worth caching under any circumstance.
|
101
|
-
#
|
102
|
-
#
|
110
|
+
# Determine if the response is worth caching under any circumstance. Responses
|
111
|
+
# marked "private" with an explicit Cache-Control directive are considered
|
112
|
+
# uncacheable
|
103
113
|
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
# cache that only serves fresh or valid objects.
|
114
|
+
# Responses with neither a freshness lifetime (Expires, max-age) nor cache
|
115
|
+
# validator (Last-Modified, Etag) are considered uncacheable.
|
107
116
|
def cacheable?
|
108
117
|
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
|
109
|
-
return false if no_store?
|
118
|
+
return false if no_store? || private?
|
110
119
|
validateable? || fresh?
|
111
120
|
end
|
112
121
|
|
@@ -114,7 +123,8 @@ module Rack::Cache
|
|
114
123
|
# a +Cache-Control+ header with +max-age+ value is present or when the
|
115
124
|
# +Expires+ header is set.
|
116
125
|
def freshness_information?
|
117
|
-
header?('Expires') ||
|
126
|
+
header?('Expires') ||
|
127
|
+
!!(cache_control['s-maxage'] || cache_control['max-age'])
|
118
128
|
end
|
119
129
|
|
120
130
|
# Determine if the response includes headers that can be used to validate
|
@@ -127,7 +137,7 @@ module Rack::Cache
|
|
127
137
|
# revalidating with the origin. Note that this does not necessary imply that
|
128
138
|
# a caching agent ought not store the response in its cache.
|
129
139
|
def no_cache?
|
130
|
-
|
140
|
+
cache_control['no-cache']
|
131
141
|
end
|
132
142
|
|
133
143
|
# Indicates that the response should not be stored under any circumstances.
|
@@ -135,16 +145,51 @@ module Rack::Cache
|
|
135
145
|
cache_control['no-store']
|
136
146
|
end
|
137
147
|
|
148
|
+
# True when the response has been explicitly marked "public".
|
149
|
+
def public?
|
150
|
+
cache_control['public']
|
151
|
+
end
|
152
|
+
|
153
|
+
# Mark the response "public", making it eligible for other clients. Note
|
154
|
+
# that responses are considered "public" by default unless the request
|
155
|
+
# includes private headers (Authorization, Cookie).
|
156
|
+
def public=(value)
|
157
|
+
value = value ? true : nil
|
158
|
+
self.cache_control = cache_control.
|
159
|
+
merge('public' => value, 'private' => !value)
|
160
|
+
end
|
161
|
+
|
162
|
+
# True when the response has been marked "private" explicitly.
|
163
|
+
def private?
|
164
|
+
cache_control['private']
|
165
|
+
end
|
166
|
+
|
167
|
+
# Mark the response "private", making it ineligible for serving other
|
168
|
+
# clients.
|
169
|
+
def private=(value)
|
170
|
+
value = value ? true : nil
|
171
|
+
self.cache_control = cache_control.
|
172
|
+
merge('public' => !value, 'private' => value)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Indicates that the cache must not serve a stale response in any
|
176
|
+
# circumstance without first revalidating with the origin. When present,
|
177
|
+
# the TTL of the response should not be overriden to be greater than the
|
178
|
+
# value provided by the origin.
|
179
|
+
def must_revalidate?
|
180
|
+
cache_control['must-revalidate'] ||
|
181
|
+
cache_control['proxy-revalidate']
|
182
|
+
end
|
183
|
+
|
138
184
|
# The date, as specified by the Date header. When no Date header is present,
|
139
185
|
# set the Date header to Time.now and return.
|
140
186
|
def date
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
187
|
+
if date = headers['Date']
|
188
|
+
Time.httpdate(date)
|
189
|
+
else
|
190
|
+
headers['Date'] = now.httpdate unless headers.frozen?
|
191
|
+
now
|
192
|
+
end
|
148
193
|
end
|
149
194
|
|
150
195
|
# The age of the response.
|
@@ -154,29 +199,36 @@ module Rack::Cache
|
|
154
199
|
|
155
200
|
# The number of seconds after the time specified in the response's Date
|
156
201
|
# header when the the response should no longer be considered fresh. First
|
157
|
-
# check for a
|
158
|
-
# header; return nil when no maximum age can be
|
202
|
+
# check for a s-maxage directive, then a max-age directive, and then fall
|
203
|
+
# back on an expires header; return nil when no maximum age can be
|
204
|
+
# established.
|
159
205
|
def max_age
|
160
|
-
if age = cache_control['max-age']
|
206
|
+
if age = (cache_control['s-maxage'] || cache_control['max-age'])
|
161
207
|
age.to_i
|
162
208
|
elsif headers['Expires']
|
163
209
|
Time.httpdate(headers['Expires']) - date
|
164
210
|
end
|
165
211
|
end
|
166
212
|
|
167
|
-
#
|
168
|
-
# be considered fresh.
|
213
|
+
# The number of seconds after which the response should no longer
|
214
|
+
# be considered fresh. Sets the Cache-Control max-age directive.
|
169
215
|
def max_age=(value)
|
170
216
|
self.cache_control = cache_control.merge('max-age' => value.to_s)
|
171
217
|
end
|
172
218
|
|
219
|
+
# Like #max_age= but sets the s-maxage directive, which applies only
|
220
|
+
# to shared caches.
|
221
|
+
def shared_max_age=(value)
|
222
|
+
self.cache_control = cache_control.merge('s-maxage' => value.to_s)
|
223
|
+
end
|
224
|
+
|
173
225
|
# The Time when the response should be considered stale. With a
|
174
226
|
# Cache-Control/max-age value is present, this is calculated by adding the
|
175
227
|
# number of seconds specified to the responses #date value. Falls back to
|
176
228
|
# the time specified in the Expires header or returns nil if neither is
|
177
229
|
# present.
|
178
230
|
def expires_at
|
179
|
-
if max_age = cache_control['max-age']
|
231
|
+
if max_age = (cache_control['s-maxage'] || cache_control['max-age'])
|
180
232
|
date + max_age.to_i
|
181
233
|
elsif time = headers['Expires']
|
182
234
|
Time.httpdate(time)
|
@@ -191,9 +243,15 @@ module Rack::Cache
|
|
191
243
|
max_age - age if max_age
|
192
244
|
end
|
193
245
|
|
194
|
-
# Set the response's time-to-live to the specified number
|
195
|
-
# adjusts the Cache-Control/
|
246
|
+
# Set the response's time-to-live for shared caches to the specified number
|
247
|
+
# of seconds. This adjusts the Cache-Control/s-maxage directive.
|
196
248
|
def ttl=(seconds)
|
249
|
+
self.shared_max_age = age + seconds
|
250
|
+
end
|
251
|
+
|
252
|
+
# Set the response's time-to-live for private/client caches. This adjusts
|
253
|
+
# the Cache-Control/max-age directive.
|
254
|
+
def client_ttl=(seconds)
|
197
255
|
self.max_age = age + seconds
|
198
256
|
end
|
199
257
|
|
@@ -210,8 +268,38 @@ module Rack::Cache
|
|
210
268
|
time_value && last_modified == time_value
|
211
269
|
end
|
212
270
|
|
213
|
-
#
|
214
|
-
#
|
271
|
+
# Determine if response's ETag matches the etag value provided. Return
|
272
|
+
# false when either value is nil.
|
273
|
+
def etag_matches?(etag)
|
274
|
+
etag && self.etag == etag
|
275
|
+
end
|
276
|
+
|
277
|
+
# Headers that MUST NOT be included with 304 Not Modified responses.
|
278
|
+
#
|
279
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
280
|
+
NOT_MODIFIED_OMIT_HEADERS = %w[
|
281
|
+
Allow
|
282
|
+
Content-Encoding
|
283
|
+
Content-Language
|
284
|
+
Content-Length
|
285
|
+
Content-Md5
|
286
|
+
Content-Type
|
287
|
+
Last-Modified
|
288
|
+
].to_set
|
289
|
+
|
290
|
+
# Modify the response so that it conforms to the rules defined for
|
291
|
+
# '304 Not Modified'. This sets the status, removes the body, and
|
292
|
+
# discards any headers that MUST NOT be included in 304 responses.
|
293
|
+
#
|
294
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
295
|
+
def not_modified!
|
296
|
+
self.status = 304
|
297
|
+
self.body = []
|
298
|
+
NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
|
302
|
+
# The literal value of the Vary header, or nil when no header is present.
|
215
303
|
def vary
|
216
304
|
headers['Vary']
|
217
305
|
end
|
data/lib/rack/cache/metastore.rb
CHANGED
@@ -21,18 +21,6 @@ module Rack::Cache
|
|
21
21
|
# methods dumb and straight-forward to implement.
|
22
22
|
class MetaStore
|
23
23
|
|
24
|
-
# Headers that should not be stored in cache (from RFC 2616).
|
25
|
-
HEADER_BLACKLIST = Set.new(%w[
|
26
|
-
Connection
|
27
|
-
Keep-Alive
|
28
|
-
Proxy-Authenticate
|
29
|
-
Proxy-Authorization
|
30
|
-
TE
|
31
|
-
Trailers
|
32
|
-
Transfer-Encoding
|
33
|
-
Upgrade
|
34
|
-
])
|
35
|
-
|
36
24
|
# Locate a cached response for the request provided. Returns a
|
37
25
|
# Rack::Cache::Response object if the cache hits or nil if no cache entry
|
38
26
|
# was found.
|
@@ -44,18 +32,18 @@ module Rack::Cache
|
|
44
32
|
|
45
33
|
# find a cached entry that matches the request.
|
46
34
|
env = request.env
|
47
|
-
match = entries.detect{
|
48
|
-
if match
|
49
|
-
# TODO what if body doesn't exist in entity store?
|
50
|
-
# reconstruct response object
|
51
|
-
req, res = match
|
52
|
-
status = res['X-Status']
|
53
|
-
body = entity_store.open(res['X-Content-Digest'])
|
54
|
-
response = Rack::Cache::Response.new(status.to_i, res, body)
|
55
|
-
response.activate!
|
35
|
+
match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
|
36
|
+
return nil if match.nil?
|
56
37
|
|
57
|
-
|
38
|
+
req, res = match
|
39
|
+
if body = entity_store.open(res['X-Content-Digest'])
|
40
|
+
response = Rack::Cache::Response.new(res['X-Status'].to_i, res, body)
|
41
|
+
response.activate!
|
58
42
|
response
|
43
|
+
else
|
44
|
+
# TODO the metastore referenced an entity that doesn't exist in
|
45
|
+
# the entitystore. we definitely want to return nil but we should
|
46
|
+
# also purge the entry from the meta-store when this is detected.
|
59
47
|
end
|
60
48
|
end
|
61
49
|
|
@@ -67,25 +55,27 @@ module Rack::Cache
|
|
67
55
|
def store(request, response, entity_store)
|
68
56
|
key = request.fullpath
|
69
57
|
stored_env = persist_request(request)
|
70
|
-
stored_response = persist_response(response)
|
71
58
|
|
72
59
|
# write the response body to the entity store if this is the
|
73
60
|
# original response.
|
74
|
-
|
61
|
+
response['X-Status'] = response.status.to_s
|
62
|
+
if response['X-Content-Digest'].nil?
|
75
63
|
digest, size = entity_store.write(response.body)
|
76
|
-
|
77
|
-
|
64
|
+
response['X-Content-Digest'] = digest
|
65
|
+
response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
|
78
66
|
response.body = entity_store.open(digest)
|
67
|
+
response.activate!
|
79
68
|
end
|
80
69
|
|
81
70
|
# read existing cache entries, remove non-varying, and add this one to
|
82
71
|
# the list
|
83
|
-
vary =
|
72
|
+
vary = response.vary
|
84
73
|
entries =
|
85
74
|
read(key).reject do |env,res|
|
86
|
-
(vary == res['Vary']) &&
|
75
|
+
(vary == res['Vary']) &&
|
76
|
+
requests_match?(vary, env, stored_env)
|
87
77
|
end
|
88
|
-
entries.unshift [stored_env,
|
78
|
+
entries.unshift [stored_env, {}.update(response.headers)]
|
89
79
|
write key, entries
|
90
80
|
end
|
91
81
|
|
@@ -99,15 +89,6 @@ module Rack::Cache
|
|
99
89
|
env
|
100
90
|
end
|
101
91
|
|
102
|
-
# Extract the headers Hash from +response+ while making any
|
103
|
-
# necessary modifications in preparation for persistence. The Hash
|
104
|
-
# returned must be marshalable.
|
105
|
-
def persist_response(response)
|
106
|
-
headers = response.headers.reject { |k,v| HEADER_BLACKLIST.include?(k) }
|
107
|
-
headers['X-Status'] = response.status.to_s
|
108
|
-
headers
|
109
|
-
end
|
110
|
-
|
111
92
|
# Determine whether the two environment hashes are non-varying based on
|
112
93
|
# the vary response header value provided.
|
113
94
|
def requests_match?(vary, env1, env2)
|
@@ -158,7 +139,9 @@ module Rack::Cache
|
|
158
139
|
end
|
159
140
|
|
160
141
|
def read(key)
|
161
|
-
@hash.fetch(key, [])
|
142
|
+
@hash.fetch(key, []).collect do |req,res|
|
143
|
+
[req.dup, res.dup]
|
144
|
+
end
|
162
145
|
end
|
163
146
|
|
164
147
|
def write(key, entries)
|
data/lib/rack/cache/options.rb
CHANGED
@@ -61,6 +61,15 @@ module Rack::Cache
|
|
61
61
|
# Default: 0
|
62
62
|
option_accessor :default_ttl
|
63
63
|
|
64
|
+
# Set of request headers that trigger "private" cache-control behavior
|
65
|
+
# on responses that don't explicitly state whether the response is
|
66
|
+
# public or private via a Cache-Control directive. Applications that use
|
67
|
+
# cookies for authorization may need to add the 'Cookie' header to this
|
68
|
+
# list.
|
69
|
+
#
|
70
|
+
# Default: ['Authorization', 'Cookie']
|
71
|
+
option_accessor :private_headers
|
72
|
+
|
64
73
|
# The underlying options Hash. During initialization (or outside of a
|
65
74
|
# request), this is a default values Hash. During a request, this is the
|
66
75
|
# Rack environment Hash. The default values Hash is merged in underneath
|
@@ -111,7 +120,8 @@ module Rack::Cache
|
|
111
120
|
'rack-cache.storage' => Rack::Cache::Storage.instance,
|
112
121
|
'rack-cache.metastore' => 'heap:/',
|
113
122
|
'rack-cache.entitystore' => 'heap:/',
|
114
|
-
'rack-cache.default_ttl' => 0
|
123
|
+
'rack-cache.default_ttl' => 0,
|
124
|
+
'rack-cache.private_headers' => ['Authorization', 'Cookie']
|
115
125
|
}
|
116
126
|
self.options = options
|
117
127
|
end
|
data/lib/rack/cache/response.rb
CHANGED
@@ -34,7 +34,7 @@ module Rack::Cache
|
|
34
34
|
# and body.
|
35
35
|
def initialize(status, headers, body)
|
36
36
|
@status = status
|
37
|
-
@headers = headers
|
37
|
+
@headers = Rack::Utils::HeaderHash.new(headers)
|
38
38
|
@body = body
|
39
39
|
@now = Time.now
|
40
40
|
@headers['Date'] ||= now.httpdate
|
@@ -62,7 +62,7 @@ module Rack::Cache
|
|
62
62
|
|
63
63
|
# Return the status, headers, and body in a three-tuple.
|
64
64
|
def to_a
|
65
|
-
[status, headers, body]
|
65
|
+
[status, headers.to_hash, body]
|
66
66
|
end
|
67
67
|
|
68
68
|
# Freezes
|