rack-cache 1.5.1 → 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES +10 -0
- data/README.md +28 -9
- data/lib/rack/cache.rb +1 -1
- data/lib/rack/cache/app_engine.rb +48 -0
- data/lib/rack/cache/appengine.rb +2 -52
- data/lib/rack/cache/cache_control.rb +209 -0
- data/lib/rack/cache/cachecontrol.rb +2 -208
- data/lib/rack/cache/entity_store.rb +377 -0
- data/lib/rack/cache/entitystore.rb +2 -341
- data/lib/rack/cache/meta_store.rb +429 -0
- data/lib/rack/cache/metastore.rb +2 -418
- data/lib/rack/cache/request.rb +1 -1
- data/lib/rack/cache/response.rb +1 -1
- data/lib/rack/cache/storage.rb +4 -2
- metadata +6 -2
@@ -0,0 +1,429 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'rack/utils'
|
4
|
+
require 'rack/cache/key'
|
5
|
+
|
6
|
+
module Rack::Cache
|
7
|
+
|
8
|
+
# The MetaStore is responsible for storing meta information about a
|
9
|
+
# request/response pair keyed by the request's URL.
|
10
|
+
#
|
11
|
+
# The meta store keeps a list of request/response pairs for each canonical
|
12
|
+
# request URL. A request/response pair is a two element Array of the form:
|
13
|
+
# [request, response]
|
14
|
+
#
|
15
|
+
# The +request+ element is a Hash of Rack environment keys. Only protocol
|
16
|
+
# keys (i.e., those that start with "HTTP_") are stored. The +response+
|
17
|
+
# element is a Hash of cached HTTP response headers for the paired request.
|
18
|
+
#
|
19
|
+
# The MetaStore class is abstract and should not be instanstiated
|
20
|
+
# directly. Concrete subclasses should implement the protected #read,
|
21
|
+
# #write, and #purge methods. Care has been taken to keep these low-level
|
22
|
+
# methods dumb and straight-forward to implement.
|
23
|
+
class MetaStore
|
24
|
+
|
25
|
+
# Locate a cached response for the request provided. Returns a
|
26
|
+
# Rack::Cache::Response object if the cache hits or nil if no cache entry
|
27
|
+
# was found.
|
28
|
+
def lookup(request, entity_store)
|
29
|
+
key = cache_key(request)
|
30
|
+
entries = read(key)
|
31
|
+
|
32
|
+
# bail out if we have nothing cached
|
33
|
+
return nil if entries.empty?
|
34
|
+
|
35
|
+
# find a cached entry that matches the request.
|
36
|
+
env = request.env
|
37
|
+
match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
|
38
|
+
return nil if match.nil?
|
39
|
+
|
40
|
+
_, res = match
|
41
|
+
if body = entity_store.open(res['X-Content-Digest'])
|
42
|
+
restore_response(res, body)
|
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.
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Write a cache entry to the store under the given key. Existing
|
51
|
+
# entries are read and any that match the response are removed.
|
52
|
+
# This method calls #write with the new list of cache entries.
|
53
|
+
def store(request, response, entity_store)
|
54
|
+
key = cache_key(request)
|
55
|
+
stored_env = persist_request(request)
|
56
|
+
|
57
|
+
# write the response body to the entity store if this is the
|
58
|
+
# original response.
|
59
|
+
if response.headers['X-Content-Digest'].nil?
|
60
|
+
if request.env['rack-cache.use_native_ttl'] && response.fresh?
|
61
|
+
digest, size = entity_store.write(response.body, response.ttl)
|
62
|
+
else
|
63
|
+
digest, size = entity_store.write(response.body)
|
64
|
+
end
|
65
|
+
response.headers['X-Content-Digest'] = digest
|
66
|
+
response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
|
67
|
+
|
68
|
+
# If the entitystore backend is a Noop, do not try to read the body from the backend, it always returns an empty array
|
69
|
+
unless entity_store.is_a? Rack::Cache::EntityStore::Noop
|
70
|
+
# A stream body can only be read once and is currently closed by #write.
|
71
|
+
# (To avoid having to keep giant objects in memory when writing to disk cache
|
72
|
+
# the body is never converted to a single string)
|
73
|
+
# We cannot always reply on body to be re-readable,
|
74
|
+
# so we have to read it from the cache.
|
75
|
+
# BUG: if the cache was unable to store a stream, the stream will be closed
|
76
|
+
# and rack will try to read it again, resulting in hard to track down exception
|
77
|
+
response.body = entity_store.open(digest) || response.body
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# read existing cache entries, remove non-varying, and add this one to
|
82
|
+
# the list
|
83
|
+
vary = response.vary
|
84
|
+
entries =
|
85
|
+
read(key).reject do |env,res|
|
86
|
+
(vary == res['Vary']) &&
|
87
|
+
requests_match?(vary, env, stored_env)
|
88
|
+
end
|
89
|
+
|
90
|
+
headers = persist_response(response)
|
91
|
+
headers.delete 'Age'
|
92
|
+
|
93
|
+
entries.unshift [stored_env, headers]
|
94
|
+
write key, entries
|
95
|
+
key
|
96
|
+
end
|
97
|
+
|
98
|
+
# Generate a cache key for the request.
|
99
|
+
def cache_key(request)
|
100
|
+
keygen = request.env['rack-cache.cache_key'] || Key
|
101
|
+
keygen.call(request)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Invalidate all cache entries that match the request.
|
105
|
+
def invalidate(request, entity_store)
|
106
|
+
modified = false
|
107
|
+
key = cache_key(request)
|
108
|
+
entries =
|
109
|
+
read(key).map do |req, res|
|
110
|
+
response = restore_response(res)
|
111
|
+
if response.fresh?
|
112
|
+
response.expire!
|
113
|
+
modified = true
|
114
|
+
[req, persist_response(response)]
|
115
|
+
else
|
116
|
+
[req, res]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
write key, entries if modified
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Extract the environment Hash from +request+ while making any
|
125
|
+
# necessary modifications in preparation for persistence. The Hash
|
126
|
+
# returned must be marshalable.
|
127
|
+
def persist_request(request)
|
128
|
+
env = request.env.dup
|
129
|
+
env.reject! { |key,val| key =~ /[^0-9A-Z_]/ || !val.respond_to?(:to_str) }
|
130
|
+
env
|
131
|
+
end
|
132
|
+
|
133
|
+
# Converts a stored response hash into a Response object. The caller
|
134
|
+
# is responsible for loading and passing the body if needed.
|
135
|
+
def restore_response(hash, body=[])
|
136
|
+
status = hash.delete('X-Status').to_i
|
137
|
+
Rack::Cache::Response.new(status, hash, body)
|
138
|
+
end
|
139
|
+
|
140
|
+
def persist_response(response)
|
141
|
+
hash = response.headers.to_hash
|
142
|
+
hash['X-Status'] = response.status.to_s
|
143
|
+
hash
|
144
|
+
end
|
145
|
+
|
146
|
+
# Determine whether the two environment hashes are non-varying based on
|
147
|
+
# the vary response header value provided.
|
148
|
+
def requests_match?(vary, env1, env2)
|
149
|
+
return true if vary.nil? || vary == ''
|
150
|
+
vary.split(/[\s,]+/).all? do |header|
|
151
|
+
key = "HTTP_#{header.upcase.tr('-', '_')}"
|
152
|
+
env1[key] == env2[key]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
protected
|
157
|
+
# Locate all cached request/response pairs that match the specified
|
158
|
+
# URL key. The result must be an Array of all cached request/response
|
159
|
+
# pairs. An empty Array must be returned if nothing is cached for
|
160
|
+
# the specified key.
|
161
|
+
def read(key)
|
162
|
+
raise NotImplementedError
|
163
|
+
end
|
164
|
+
|
165
|
+
# Store an Array of request/response pairs for the given key. Concrete
|
166
|
+
# implementations should not attempt to filter or concatenate the
|
167
|
+
# list in any way.
|
168
|
+
def write(key, negotiations)
|
169
|
+
raise NotImplementedError
|
170
|
+
end
|
171
|
+
|
172
|
+
# Remove all cached entries at the key specified. No error is raised
|
173
|
+
# when the key does not exist.
|
174
|
+
def purge(key)
|
175
|
+
raise NotImplementedError
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
# Generate a SHA1 hex digest for the specified string. This is a
|
180
|
+
# simple utility method for meta store implementations.
|
181
|
+
def hexdigest(data)
|
182
|
+
Digest::SHA1.hexdigest(data)
|
183
|
+
end
|
184
|
+
|
185
|
+
public
|
186
|
+
# Concrete MetaStore implementation that uses a simple Hash to store
|
187
|
+
# request/response pairs on the heap.
|
188
|
+
class Heap < MetaStore
|
189
|
+
def initialize(hash={})
|
190
|
+
@hash = hash
|
191
|
+
end
|
192
|
+
|
193
|
+
def read(key)
|
194
|
+
if data = @hash[key]
|
195
|
+
Marshal.load(data)
|
196
|
+
else
|
197
|
+
[]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def write(key, entries)
|
202
|
+
@hash[key] = Marshal.dump(entries)
|
203
|
+
end
|
204
|
+
|
205
|
+
def purge(key)
|
206
|
+
@hash.delete(key)
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
|
210
|
+
def to_hash
|
211
|
+
@hash
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.resolve(uri)
|
215
|
+
new
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
HEAP = Heap
|
220
|
+
MEM = HEAP
|
221
|
+
|
222
|
+
# Concrete MetaStore implementation that stores request/response
|
223
|
+
# pairs on disk.
|
224
|
+
class Disk < MetaStore
|
225
|
+
attr_reader :root
|
226
|
+
|
227
|
+
def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
|
228
|
+
@root = File.expand_path(root)
|
229
|
+
FileUtils.mkdir_p(root, :mode => 0755)
|
230
|
+
end
|
231
|
+
|
232
|
+
def read(key)
|
233
|
+
path = key_path(key)
|
234
|
+
File.open(path, 'rb') { |io| Marshal.load(io) }
|
235
|
+
rescue Errno::ENOENT, IOError
|
236
|
+
[]
|
237
|
+
end
|
238
|
+
|
239
|
+
def write(key, entries)
|
240
|
+
tries = 0
|
241
|
+
begin
|
242
|
+
path = key_path(key)
|
243
|
+
File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
|
244
|
+
rescue Errno::ENOENT, IOError
|
245
|
+
Dir.mkdir(File.dirname(path), 0755)
|
246
|
+
retry if (tries += 1) == 1
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def purge(key)
|
251
|
+
path = key_path(key)
|
252
|
+
File.unlink(path)
|
253
|
+
nil
|
254
|
+
rescue Errno::ENOENT, IOError
|
255
|
+
nil
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
def key_path(key)
|
260
|
+
File.join(root, spread(hexdigest(key)))
|
261
|
+
end
|
262
|
+
|
263
|
+
def spread(sha, n=2)
|
264
|
+
sha = sha.dup
|
265
|
+
sha[n,0] = '/'
|
266
|
+
sha
|
267
|
+
end
|
268
|
+
|
269
|
+
public
|
270
|
+
def self.resolve(uri)
|
271
|
+
path = File.expand_path(uri.opaque || uri.path)
|
272
|
+
new path
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
276
|
+
|
277
|
+
DISK = Disk
|
278
|
+
FILE = Disk
|
279
|
+
|
280
|
+
# Stores request/response pairs in memcached. Keys are not stored
|
281
|
+
# directly since memcached has a 250-byte limit on key names. Instead,
|
282
|
+
# the SHA1 hexdigest of the key is used.
|
283
|
+
class MemCacheBase < MetaStore
|
284
|
+
extend Rack::Utils
|
285
|
+
|
286
|
+
# The MemCache object used to communicated with the memcached
|
287
|
+
# daemon.
|
288
|
+
attr_reader :cache
|
289
|
+
|
290
|
+
# Create MemCache store for the given URI. The URI must specify
|
291
|
+
# a host and may specify a port, namespace, and options:
|
292
|
+
#
|
293
|
+
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
|
294
|
+
#
|
295
|
+
# Query parameter names and values are documented with the memcached
|
296
|
+
# library: http://tinyurl.com/4upqnd
|
297
|
+
def self.resolve(uri)
|
298
|
+
if uri.respond_to?(:scheme)
|
299
|
+
server = "#{uri.host}:#{uri.port || '11211'}"
|
300
|
+
options = parse_query(uri.query)
|
301
|
+
options.keys.each do |key|
|
302
|
+
value =
|
303
|
+
case value = options.delete(key)
|
304
|
+
when 'true' ; true
|
305
|
+
when 'false' ; false
|
306
|
+
else value.to_sym
|
307
|
+
end
|
308
|
+
options[key.to_sym] = value
|
309
|
+
end
|
310
|
+
|
311
|
+
options[:namespace] = uri.path.to_s.sub(/^\//, '')
|
312
|
+
|
313
|
+
new server, options
|
314
|
+
else
|
315
|
+
# if the object provided is not a URI, pass it straight through
|
316
|
+
# to the underlying implementation.
|
317
|
+
new uri
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
class Dalli < MemCacheBase
|
323
|
+
def initialize(server="localhost:11211", options={})
|
324
|
+
@cache =
|
325
|
+
if server.respond_to?(:stats)
|
326
|
+
server
|
327
|
+
else
|
328
|
+
require 'dalli'
|
329
|
+
::Dalli::Client.new(server, options)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def read(key)
|
334
|
+
key = hexdigest(key)
|
335
|
+
cache.get(key) || []
|
336
|
+
end
|
337
|
+
|
338
|
+
def write(key, entries)
|
339
|
+
key = hexdigest(key)
|
340
|
+
cache.set(key, entries)
|
341
|
+
end
|
342
|
+
|
343
|
+
def purge(key)
|
344
|
+
cache.delete(hexdigest(key))
|
345
|
+
nil
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
class MemCached < MemCacheBase
|
350
|
+
# The Memcached instance used to communicated with the memcached
|
351
|
+
# daemon.
|
352
|
+
attr_reader :cache
|
353
|
+
|
354
|
+
def initialize(server="localhost:11211", options={})
|
355
|
+
options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
|
356
|
+
@cache =
|
357
|
+
if server.respond_to?(:stats)
|
358
|
+
server
|
359
|
+
else
|
360
|
+
require 'memcached'
|
361
|
+
Memcached.new(server, options)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def read(key)
|
366
|
+
key = hexdigest(key)
|
367
|
+
cache.get(key)
|
368
|
+
rescue Memcached::NotFound
|
369
|
+
[]
|
370
|
+
end
|
371
|
+
|
372
|
+
def write(key, entries)
|
373
|
+
key = hexdigest(key)
|
374
|
+
cache.set(key, entries)
|
375
|
+
end
|
376
|
+
|
377
|
+
def purge(key)
|
378
|
+
key = hexdigest(key)
|
379
|
+
cache.delete(key)
|
380
|
+
nil
|
381
|
+
rescue Memcached::NotFound
|
382
|
+
nil
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
MEMCACHE =
|
387
|
+
if defined?(::Memcached)
|
388
|
+
MemCached
|
389
|
+
else
|
390
|
+
Dalli
|
391
|
+
end
|
392
|
+
MEMCACHED = MEMCACHE
|
393
|
+
|
394
|
+
class GAEStore < MetaStore
|
395
|
+
attr_reader :cache
|
396
|
+
|
397
|
+
def initialize(options = {})
|
398
|
+
require 'rack/cache/app_engine'
|
399
|
+
@cache = Rack::Cache::AppEngine::MemCache.new(options)
|
400
|
+
end
|
401
|
+
|
402
|
+
def read(key)
|
403
|
+
key = hexdigest(key)
|
404
|
+
cache.get(key) || []
|
405
|
+
end
|
406
|
+
|
407
|
+
def write(key, entries)
|
408
|
+
key = hexdigest(key)
|
409
|
+
cache.put(key, entries)
|
410
|
+
end
|
411
|
+
|
412
|
+
def purge(key)
|
413
|
+
key = hexdigest(key)
|
414
|
+
cache.delete(key)
|
415
|
+
nil
|
416
|
+
end
|
417
|
+
|
418
|
+
def self.resolve(uri)
|
419
|
+
self.new(:namespace => uri.host)
|
420
|
+
end
|
421
|
+
|
422
|
+
end
|
423
|
+
|
424
|
+
GAECACHE = GAEStore
|
425
|
+
GAE = GAEStore
|
426
|
+
|
427
|
+
end
|
428
|
+
|
429
|
+
end
|
data/lib/rack/cache/metastore.rb
CHANGED
@@ -1,418 +1,2 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'rack/utils'
|
4
|
-
require 'rack/cache/key'
|
5
|
-
|
6
|
-
module Rack::Cache
|
7
|
-
|
8
|
-
# The MetaStore is responsible for storing meta information about a
|
9
|
-
# request/response pair keyed by the request's URL.
|
10
|
-
#
|
11
|
-
# The meta store keeps a list of request/response pairs for each canonical
|
12
|
-
# request URL. A request/response pair is a two element Array of the form:
|
13
|
-
# [request, response]
|
14
|
-
#
|
15
|
-
# The +request+ element is a Hash of Rack environment keys. Only protocol
|
16
|
-
# keys (i.e., those that start with "HTTP_") are stored. The +response+
|
17
|
-
# element is a Hash of cached HTTP response headers for the paired request.
|
18
|
-
#
|
19
|
-
# The MetaStore class is abstract and should not be instanstiated
|
20
|
-
# directly. Concrete subclasses should implement the protected #read,
|
21
|
-
# #write, and #purge methods. Care has been taken to keep these low-level
|
22
|
-
# methods dumb and straight-forward to implement.
|
23
|
-
class MetaStore
|
24
|
-
|
25
|
-
# Locate a cached response for the request provided. Returns a
|
26
|
-
# Rack::Cache::Response object if the cache hits or nil if no cache entry
|
27
|
-
# was found.
|
28
|
-
def lookup(request, entity_store)
|
29
|
-
key = cache_key(request)
|
30
|
-
entries = read(key)
|
31
|
-
|
32
|
-
# bail out if we have nothing cached
|
33
|
-
return nil if entries.empty?
|
34
|
-
|
35
|
-
# find a cached entry that matches the request.
|
36
|
-
env = request.env
|
37
|
-
match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
|
38
|
-
return nil if match.nil?
|
39
|
-
|
40
|
-
_, res = match
|
41
|
-
if body = entity_store.open(res['X-Content-Digest'])
|
42
|
-
restore_response(res, body)
|
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.
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Write a cache entry to the store under the given key. Existing
|
51
|
-
# entries are read and any that match the response are removed.
|
52
|
-
# This method calls #write with the new list of cache entries.
|
53
|
-
def store(request, response, entity_store)
|
54
|
-
key = cache_key(request)
|
55
|
-
stored_env = persist_request(request)
|
56
|
-
|
57
|
-
# write the response body to the entity store if this is the
|
58
|
-
# original response.
|
59
|
-
if response.headers['X-Content-Digest'].nil?
|
60
|
-
if request.env['rack-cache.use_native_ttl'] && response.fresh?
|
61
|
-
digest, size = entity_store.write(response.body, response.ttl)
|
62
|
-
else
|
63
|
-
digest, size = entity_store.write(response.body)
|
64
|
-
end
|
65
|
-
response.headers['X-Content-Digest'] = digest
|
66
|
-
response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
|
67
|
-
response.body = entity_store.open(digest) || response.body
|
68
|
-
end
|
69
|
-
|
70
|
-
# read existing cache entries, remove non-varying, and add this one to
|
71
|
-
# the list
|
72
|
-
vary = response.vary
|
73
|
-
entries =
|
74
|
-
read(key).reject do |env,res|
|
75
|
-
(vary == res['Vary']) &&
|
76
|
-
requests_match?(vary, env, stored_env)
|
77
|
-
end
|
78
|
-
|
79
|
-
headers = persist_response(response)
|
80
|
-
headers.delete 'Age'
|
81
|
-
|
82
|
-
entries.unshift [stored_env, headers]
|
83
|
-
write key, entries
|
84
|
-
key
|
85
|
-
end
|
86
|
-
|
87
|
-
# Generate a cache key for the request.
|
88
|
-
def cache_key(request)
|
89
|
-
keygen = request.env['rack-cache.cache_key'] || Key
|
90
|
-
keygen.call(request)
|
91
|
-
end
|
92
|
-
|
93
|
-
# Invalidate all cache entries that match the request.
|
94
|
-
def invalidate(request, entity_store)
|
95
|
-
modified = false
|
96
|
-
key = cache_key(request)
|
97
|
-
entries =
|
98
|
-
read(key).map do |req, res|
|
99
|
-
response = restore_response(res)
|
100
|
-
if response.fresh?
|
101
|
-
response.expire!
|
102
|
-
modified = true
|
103
|
-
[req, persist_response(response)]
|
104
|
-
else
|
105
|
-
[req, res]
|
106
|
-
end
|
107
|
-
end
|
108
|
-
write key, entries if modified
|
109
|
-
end
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
# Extract the environment Hash from +request+ while making any
|
114
|
-
# necessary modifications in preparation for persistence. The Hash
|
115
|
-
# returned must be marshalable.
|
116
|
-
def persist_request(request)
|
117
|
-
env = request.env.dup
|
118
|
-
env.reject! { |key,val| key =~ /[^0-9A-Z_]/ || !val.respond_to?(:to_str) }
|
119
|
-
env
|
120
|
-
end
|
121
|
-
|
122
|
-
# Converts a stored response hash into a Response object. The caller
|
123
|
-
# is responsible for loading and passing the body if needed.
|
124
|
-
def restore_response(hash, body=nil)
|
125
|
-
status = hash.delete('X-Status').to_i
|
126
|
-
Rack::Cache::Response.new(status, hash, body)
|
127
|
-
end
|
128
|
-
|
129
|
-
def persist_response(response)
|
130
|
-
hash = response.headers.to_hash
|
131
|
-
hash['X-Status'] = response.status.to_s
|
132
|
-
hash
|
133
|
-
end
|
134
|
-
|
135
|
-
# Determine whether the two environment hashes are non-varying based on
|
136
|
-
# the vary response header value provided.
|
137
|
-
def requests_match?(vary, env1, env2)
|
138
|
-
return true if vary.nil? || vary == ''
|
139
|
-
vary.split(/[\s,]+/).all? do |header|
|
140
|
-
key = "HTTP_#{header.upcase.tr('-', '_')}"
|
141
|
-
env1[key] == env2[key]
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
protected
|
146
|
-
# Locate all cached request/response pairs that match the specified
|
147
|
-
# URL key. The result must be an Array of all cached request/response
|
148
|
-
# pairs. An empty Array must be returned if nothing is cached for
|
149
|
-
# the specified key.
|
150
|
-
def read(key)
|
151
|
-
raise NotImplementedError
|
152
|
-
end
|
153
|
-
|
154
|
-
# Store an Array of request/response pairs for the given key. Concrete
|
155
|
-
# implementations should not attempt to filter or concatenate the
|
156
|
-
# list in any way.
|
157
|
-
def write(key, negotiations)
|
158
|
-
raise NotImplementedError
|
159
|
-
end
|
160
|
-
|
161
|
-
# Remove all cached entries at the key specified. No error is raised
|
162
|
-
# when the key does not exist.
|
163
|
-
def purge(key)
|
164
|
-
raise NotImplementedError
|
165
|
-
end
|
166
|
-
|
167
|
-
private
|
168
|
-
# Generate a SHA1 hex digest for the specified string. This is a
|
169
|
-
# simple utility method for meta store implementations.
|
170
|
-
def hexdigest(data)
|
171
|
-
Digest::SHA1.hexdigest(data)
|
172
|
-
end
|
173
|
-
|
174
|
-
public
|
175
|
-
# Concrete MetaStore implementation that uses a simple Hash to store
|
176
|
-
# request/response pairs on the heap.
|
177
|
-
class Heap < MetaStore
|
178
|
-
def initialize(hash={})
|
179
|
-
@hash = hash
|
180
|
-
end
|
181
|
-
|
182
|
-
def read(key)
|
183
|
-
if data = @hash[key]
|
184
|
-
Marshal.load(data)
|
185
|
-
else
|
186
|
-
[]
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def write(key, entries)
|
191
|
-
@hash[key] = Marshal.dump(entries)
|
192
|
-
end
|
193
|
-
|
194
|
-
def purge(key)
|
195
|
-
@hash.delete(key)
|
196
|
-
nil
|
197
|
-
end
|
198
|
-
|
199
|
-
def to_hash
|
200
|
-
@hash
|
201
|
-
end
|
202
|
-
|
203
|
-
def self.resolve(uri)
|
204
|
-
new
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
HEAP = Heap
|
209
|
-
MEM = HEAP
|
210
|
-
|
211
|
-
# Concrete MetaStore implementation that stores request/response
|
212
|
-
# pairs on disk.
|
213
|
-
class Disk < MetaStore
|
214
|
-
attr_reader :root
|
215
|
-
|
216
|
-
def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
|
217
|
-
@root = File.expand_path(root)
|
218
|
-
FileUtils.mkdir_p(root, :mode => 0755)
|
219
|
-
end
|
220
|
-
|
221
|
-
def read(key)
|
222
|
-
path = key_path(key)
|
223
|
-
File.open(path, 'rb') { |io| Marshal.load(io) }
|
224
|
-
rescue Errno::ENOENT, IOError
|
225
|
-
[]
|
226
|
-
end
|
227
|
-
|
228
|
-
def write(key, entries)
|
229
|
-
tries = 0
|
230
|
-
begin
|
231
|
-
path = key_path(key)
|
232
|
-
File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
|
233
|
-
rescue Errno::ENOENT, IOError
|
234
|
-
Dir.mkdir(File.dirname(path), 0755)
|
235
|
-
retry if (tries += 1) == 1
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
def purge(key)
|
240
|
-
path = key_path(key)
|
241
|
-
File.unlink(path)
|
242
|
-
nil
|
243
|
-
rescue Errno::ENOENT, IOError
|
244
|
-
nil
|
245
|
-
end
|
246
|
-
|
247
|
-
private
|
248
|
-
def key_path(key)
|
249
|
-
File.join(root, spread(hexdigest(key)))
|
250
|
-
end
|
251
|
-
|
252
|
-
def spread(sha, n=2)
|
253
|
-
sha = sha.dup
|
254
|
-
sha[n,0] = '/'
|
255
|
-
sha
|
256
|
-
end
|
257
|
-
|
258
|
-
public
|
259
|
-
def self.resolve(uri)
|
260
|
-
path = File.expand_path(uri.opaque || uri.path)
|
261
|
-
new path
|
262
|
-
end
|
263
|
-
|
264
|
-
end
|
265
|
-
|
266
|
-
DISK = Disk
|
267
|
-
FILE = Disk
|
268
|
-
|
269
|
-
# Stores request/response pairs in memcached. Keys are not stored
|
270
|
-
# directly since memcached has a 250-byte limit on key names. Instead,
|
271
|
-
# the SHA1 hexdigest of the key is used.
|
272
|
-
class MemCacheBase < MetaStore
|
273
|
-
extend Rack::Utils
|
274
|
-
|
275
|
-
# The MemCache object used to communicated with the memcached
|
276
|
-
# daemon.
|
277
|
-
attr_reader :cache
|
278
|
-
|
279
|
-
# Create MemCache store for the given URI. The URI must specify
|
280
|
-
# a host and may specify a port, namespace, and options:
|
281
|
-
#
|
282
|
-
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
|
283
|
-
#
|
284
|
-
# Query parameter names and values are documented with the memcached
|
285
|
-
# library: http://tinyurl.com/4upqnd
|
286
|
-
def self.resolve(uri)
|
287
|
-
if uri.respond_to?(:scheme)
|
288
|
-
server = "#{uri.host}:#{uri.port || '11211'}"
|
289
|
-
options = parse_query(uri.query)
|
290
|
-
options.keys.each do |key|
|
291
|
-
value =
|
292
|
-
case value = options.delete(key)
|
293
|
-
when 'true' ; true
|
294
|
-
when 'false' ; false
|
295
|
-
else value.to_sym
|
296
|
-
end
|
297
|
-
options[key.to_sym] = value
|
298
|
-
end
|
299
|
-
|
300
|
-
options[:namespace] = uri.path.to_s.sub(/^\//, '')
|
301
|
-
|
302
|
-
new server, options
|
303
|
-
else
|
304
|
-
# if the object provided is not a URI, pass it straight through
|
305
|
-
# to the underlying implementation.
|
306
|
-
new uri
|
307
|
-
end
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
class Dalli < MemCacheBase
|
312
|
-
def initialize(server="localhost:11211", options={})
|
313
|
-
@cache =
|
314
|
-
if server.respond_to?(:stats)
|
315
|
-
server
|
316
|
-
else
|
317
|
-
require 'dalli'
|
318
|
-
::Dalli::Client.new(server, options)
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
def read(key)
|
323
|
-
key = hexdigest(key)
|
324
|
-
cache.get(key) || []
|
325
|
-
end
|
326
|
-
|
327
|
-
def write(key, entries)
|
328
|
-
key = hexdigest(key)
|
329
|
-
cache.set(key, entries)
|
330
|
-
end
|
331
|
-
|
332
|
-
def purge(key)
|
333
|
-
cache.delete(hexdigest(key))
|
334
|
-
nil
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
class MemCached < MemCacheBase
|
339
|
-
# The Memcached instance used to communicated with the memcached
|
340
|
-
# daemon.
|
341
|
-
attr_reader :cache
|
342
|
-
|
343
|
-
def initialize(server="localhost:11211", options={})
|
344
|
-
options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
|
345
|
-
@cache =
|
346
|
-
if server.respond_to?(:stats)
|
347
|
-
server
|
348
|
-
else
|
349
|
-
require 'memcached'
|
350
|
-
Memcached.new(server, options)
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
|
-
def read(key)
|
355
|
-
key = hexdigest(key)
|
356
|
-
cache.get(key)
|
357
|
-
rescue Memcached::NotFound
|
358
|
-
[]
|
359
|
-
end
|
360
|
-
|
361
|
-
def write(key, entries)
|
362
|
-
key = hexdigest(key)
|
363
|
-
cache.set(key, entries)
|
364
|
-
end
|
365
|
-
|
366
|
-
def purge(key)
|
367
|
-
key = hexdigest(key)
|
368
|
-
cache.delete(key)
|
369
|
-
nil
|
370
|
-
rescue Memcached::NotFound
|
371
|
-
nil
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
|
-
MEMCACHE =
|
376
|
-
if defined?(::Memcached)
|
377
|
-
MemCached
|
378
|
-
else
|
379
|
-
Dalli
|
380
|
-
end
|
381
|
-
MEMCACHED = MEMCACHE
|
382
|
-
|
383
|
-
class GAEStore < MetaStore
|
384
|
-
attr_reader :cache
|
385
|
-
|
386
|
-
def initialize(options = {})
|
387
|
-
require 'rack/cache/appengine'
|
388
|
-
@cache = Rack::Cache::AppEngine::MemCache.new(options)
|
389
|
-
end
|
390
|
-
|
391
|
-
def read(key)
|
392
|
-
key = hexdigest(key)
|
393
|
-
cache.get(key) || []
|
394
|
-
end
|
395
|
-
|
396
|
-
def write(key, entries)
|
397
|
-
key = hexdigest(key)
|
398
|
-
cache.put(key, entries)
|
399
|
-
end
|
400
|
-
|
401
|
-
def purge(key)
|
402
|
-
key = hexdigest(key)
|
403
|
-
cache.delete(key)
|
404
|
-
nil
|
405
|
-
end
|
406
|
-
|
407
|
-
def self.resolve(uri)
|
408
|
-
self.new(:namespace => uri.host)
|
409
|
-
end
|
410
|
-
|
411
|
-
end
|
412
|
-
|
413
|
-
GAECACHE = GAEStore
|
414
|
-
GAE = GAEStore
|
415
|
-
|
416
|
-
end
|
417
|
-
|
418
|
-
end
|
1
|
+
warn "use require 'rack/cache/meta_store'"
|
2
|
+
require 'rack/cache/meta_store'
|