josh-rack-cache 0.5.1
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/CHANGES +167 -0
- data/COPYING +18 -0
- data/README +110 -0
- data/Rakefile +137 -0
- data/TODO +27 -0
- data/doc/configuration.markdown +112 -0
- data/doc/faq.markdown +141 -0
- data/doc/index.markdown +121 -0
- data/doc/layout.html.erb +34 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/server.ru +34 -0
- data/doc/storage.markdown +164 -0
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +45 -0
- data/lib/rack/cache/appengine.rb +52 -0
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +253 -0
- data/lib/rack/cache/entitystore.rb +339 -0
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +407 -0
- data/lib/rack/cache/options.rb +150 -0
- data/lib/rack/cache/request.rb +33 -0
- data/lib/rack/cache/response.rb +267 -0
- data/lib/rack/cache/storage.rb +62 -0
- data/rack-cache.gemspec +70 -0
- data/test/cache_test.rb +38 -0
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +774 -0
- data/test/entitystore_test.rb +230 -0
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +302 -0
- data/test/options_test.rb +77 -0
- data/test/pony.jpg +0 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +178 -0
- data/test/spec_setup.rb +237 -0
- data/test/storage_test.rb +94 -0
- metadata +118 -0
@@ -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
|
@@ -0,0 +1,407 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'rack/utils'
|
4
|
+
|
5
|
+
module Rack::Cache
|
6
|
+
|
7
|
+
# The MetaStore is responsible for storing meta information about a
|
8
|
+
# request/response pair keyed by the request's URL.
|
9
|
+
#
|
10
|
+
# The meta store keeps a list of request/response pairs for each canonical
|
11
|
+
# request URL. A request/response pair is a two element Array of the form:
|
12
|
+
# [request, response]
|
13
|
+
#
|
14
|
+
# The +request+ element is a Hash of Rack environment keys. Only protocol
|
15
|
+
# keys (i.e., those that start with "HTTP_") are stored. The +response+
|
16
|
+
# element is a Hash of cached HTTP response headers for the paired request.
|
17
|
+
#
|
18
|
+
# The MetaStore class is abstract and should not be instanstiated
|
19
|
+
# directly. Concrete subclasses should implement the protected #read,
|
20
|
+
# #write, and #purge methods. Care has been taken to keep these low-level
|
21
|
+
# methods dumb and straight-forward to implement.
|
22
|
+
class MetaStore
|
23
|
+
|
24
|
+
# Locate a cached response for the request provided. Returns a
|
25
|
+
# Rack::Cache::Response object if the cache hits or nil if no cache entry
|
26
|
+
# was found.
|
27
|
+
def lookup(request, entity_store)
|
28
|
+
key = cache_key(request)
|
29
|
+
entries = read(key)
|
30
|
+
|
31
|
+
# bail out if we have nothing cached
|
32
|
+
return nil if entries.empty?
|
33
|
+
|
34
|
+
# find a cached entry that matches the request.
|
35
|
+
env = request.env
|
36
|
+
match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
|
37
|
+
return nil if match.nil?
|
38
|
+
|
39
|
+
req, res = match
|
40
|
+
if body = entity_store.open(res['X-Content-Digest'])
|
41
|
+
restore_response(res, body)
|
42
|
+
else
|
43
|
+
# TODO the metastore referenced an entity that doesn't exist in
|
44
|
+
# the entitystore. we definitely want to return nil but we should
|
45
|
+
# also purge the entry from the meta-store when this is detected.
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Write a cache entry to the store under the given key. Existing
|
50
|
+
# entries are read and any that match the response are removed.
|
51
|
+
# This method calls #write with the new list of cache entries.
|
52
|
+
def store(request, response, entity_store)
|
53
|
+
key = cache_key(request)
|
54
|
+
stored_env = persist_request(request)
|
55
|
+
|
56
|
+
# write the response body to the entity store if this is the
|
57
|
+
# original response.
|
58
|
+
if response.headers['X-Content-Digest'].nil?
|
59
|
+
digest, size = entity_store.write(response.body)
|
60
|
+
response.headers['X-Content-Digest'] = digest
|
61
|
+
response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
|
62
|
+
response.body = entity_store.open(digest)
|
63
|
+
end
|
64
|
+
|
65
|
+
# read existing cache entries, remove non-varying, and add this one to
|
66
|
+
# the list
|
67
|
+
vary = response.vary
|
68
|
+
entries =
|
69
|
+
read(key).reject do |env,res|
|
70
|
+
(vary == res['Vary']) &&
|
71
|
+
requests_match?(vary, env, stored_env)
|
72
|
+
end
|
73
|
+
|
74
|
+
headers = persist_response(response)
|
75
|
+
headers.delete 'Age'
|
76
|
+
|
77
|
+
entries.unshift [stored_env, headers]
|
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
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# Extract the environment Hash from +request+ while making any
|
109
|
+
# necessary modifications in preparation for persistence. The Hash
|
110
|
+
# returned must be marshalable.
|
111
|
+
def persist_request(request)
|
112
|
+
env = request.env.dup
|
113
|
+
env.reject! { |key,val| key =~ /[^0-9A-Z_]/ }
|
114
|
+
env
|
115
|
+
end
|
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
|
+
|
130
|
+
# Determine whether the two environment hashes are non-varying based on
|
131
|
+
# the vary response header value provided.
|
132
|
+
def requests_match?(vary, env1, env2)
|
133
|
+
return true if vary.nil? || vary == ''
|
134
|
+
vary.split(/[\s,]+/).all? do |header|
|
135
|
+
key = "HTTP_#{header.upcase.tr('-', '_')}"
|
136
|
+
env1[key] == env2[key]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
# Locate all cached request/response pairs that match the specified
|
142
|
+
# URL key. The result must be an Array of all cached request/response
|
143
|
+
# pairs. An empty Array must be returned if nothing is cached for
|
144
|
+
# the specified key.
|
145
|
+
def read(key)
|
146
|
+
raise NotImplemented
|
147
|
+
end
|
148
|
+
|
149
|
+
# Store an Array of request/response pairs for the given key. Concrete
|
150
|
+
# implementations should not attempt to filter or concatenate the
|
151
|
+
# list in any way.
|
152
|
+
def write(key, negotiations)
|
153
|
+
raise NotImplemented
|
154
|
+
end
|
155
|
+
|
156
|
+
# Remove all cached entries at the key specified. No error is raised
|
157
|
+
# when the key does not exist.
|
158
|
+
def purge(key)
|
159
|
+
raise NotImplemented
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
# Generate a SHA1 hex digest for the specified string. This is a
|
164
|
+
# simple utility method for meta store implementations.
|
165
|
+
def hexdigest(data)
|
166
|
+
Digest::SHA1.hexdigest(data)
|
167
|
+
end
|
168
|
+
|
169
|
+
public
|
170
|
+
# Concrete MetaStore implementation that uses a simple Hash to store
|
171
|
+
# request/response pairs on the heap.
|
172
|
+
class Heap < MetaStore
|
173
|
+
def initialize(hash={})
|
174
|
+
@hash = hash
|
175
|
+
end
|
176
|
+
|
177
|
+
def read(key)
|
178
|
+
@hash.fetch(key, []).collect do |req,res|
|
179
|
+
[req.dup, res.dup]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def write(key, entries)
|
184
|
+
@hash[key] = entries
|
185
|
+
end
|
186
|
+
|
187
|
+
def purge(key)
|
188
|
+
@hash.delete(key)
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def to_hash
|
193
|
+
@hash
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.resolve(uri)
|
197
|
+
new
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
HEAP = Heap
|
202
|
+
MEM = HEAP
|
203
|
+
|
204
|
+
# Concrete MetaStore implementation that stores request/response
|
205
|
+
# pairs on disk.
|
206
|
+
class Disk < MetaStore
|
207
|
+
attr_reader :root
|
208
|
+
|
209
|
+
def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
|
210
|
+
@root = File.expand_path(root)
|
211
|
+
FileUtils.mkdir_p(root, :mode => 0755)
|
212
|
+
end
|
213
|
+
|
214
|
+
def read(key)
|
215
|
+
path = key_path(key)
|
216
|
+
File.open(path, 'rb') { |io| Marshal.load(io) }
|
217
|
+
rescue Errno::ENOENT
|
218
|
+
[]
|
219
|
+
end
|
220
|
+
|
221
|
+
def write(key, entries)
|
222
|
+
path = key_path(key)
|
223
|
+
File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
|
224
|
+
rescue Errno::ENOENT
|
225
|
+
Dir.mkdir(File.dirname(path), 0755)
|
226
|
+
retry
|
227
|
+
end
|
228
|
+
|
229
|
+
def purge(key)
|
230
|
+
path = key_path(key)
|
231
|
+
File.unlink(path)
|
232
|
+
nil
|
233
|
+
rescue Errno::ENOENT
|
234
|
+
nil
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
def key_path(key)
|
239
|
+
File.join(root, spread(hexdigest(key)))
|
240
|
+
end
|
241
|
+
|
242
|
+
def spread(sha, n=2)
|
243
|
+
sha = sha.dup
|
244
|
+
sha[n,0] = '/'
|
245
|
+
sha
|
246
|
+
end
|
247
|
+
|
248
|
+
public
|
249
|
+
def self.resolve(uri)
|
250
|
+
path = File.expand_path(uri.opaque || uri.path)
|
251
|
+
new path
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
DISK = Disk
|
257
|
+
FILE = Disk
|
258
|
+
|
259
|
+
# Stores request/response pairs in memcached. Keys are not stored
|
260
|
+
# directly since memcached has a 250-byte limit on key names. Instead,
|
261
|
+
# the SHA1 hexdigest of the key is used.
|
262
|
+
class MemCacheBase < MetaStore
|
263
|
+
extend Rack::Utils
|
264
|
+
|
265
|
+
# The MemCache object used to communicated with the memcached
|
266
|
+
# daemon.
|
267
|
+
attr_reader :cache
|
268
|
+
|
269
|
+
# Create MemCache store for the given URI. The URI must specify
|
270
|
+
# a host and may specify a port, namespace, and options:
|
271
|
+
#
|
272
|
+
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
|
273
|
+
#
|
274
|
+
# Query parameter names and values are documented with the memcached
|
275
|
+
# library: http://tinyurl.com/4upqnd
|
276
|
+
def self.resolve(uri)
|
277
|
+
if uri.respond_to?(:scheme)
|
278
|
+
server = "#{uri.host}:#{uri.port || '11211'}"
|
279
|
+
options = parse_query(uri.query)
|
280
|
+
options.keys.each do |key|
|
281
|
+
value =
|
282
|
+
case value = options.delete(key)
|
283
|
+
when 'true' ; true
|
284
|
+
when 'false' ; false
|
285
|
+
else value.to_sym
|
286
|
+
end
|
287
|
+
options[k.to_sym] = value
|
288
|
+
end
|
289
|
+
|
290
|
+
options[:namespace] = uri.path.to_s.sub(/^\//, '')
|
291
|
+
|
292
|
+
new server, options
|
293
|
+
else
|
294
|
+
# if the object provided is not a URI, pass it straight through
|
295
|
+
# to the underlying implementation.
|
296
|
+
new uri
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
class MemCache < MemCacheBase
|
302
|
+
def initialize(server="localhost:11211", options={})
|
303
|
+
@cache =
|
304
|
+
if server.respond_to?(:stats)
|
305
|
+
server
|
306
|
+
else
|
307
|
+
require 'memcache'
|
308
|
+
::MemCache.new(server, options)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def read(key)
|
313
|
+
key = hexdigest(key)
|
314
|
+
cache.get(key) || []
|
315
|
+
end
|
316
|
+
|
317
|
+
def write(key, entries)
|
318
|
+
key = hexdigest(key)
|
319
|
+
cache.set(key, entries)
|
320
|
+
end
|
321
|
+
|
322
|
+
def purge(key)
|
323
|
+
cache.delete(hexdigest(key))
|
324
|
+
nil
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
class MemCached < MemCacheBase
|
329
|
+
# The Memcached instance used to communicated with the memcached
|
330
|
+
# daemon.
|
331
|
+
attr_reader :cache
|
332
|
+
|
333
|
+
def initialize(server="localhost:11211", options={})
|
334
|
+
@cache =
|
335
|
+
if server.respond_to?(:stats)
|
336
|
+
server
|
337
|
+
else
|
338
|
+
require 'memcached'
|
339
|
+
Memcached.new(server, options)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def read(key)
|
344
|
+
key = hexdigest(key)
|
345
|
+
cache.get(key)
|
346
|
+
rescue Memcached::NotFound
|
347
|
+
[]
|
348
|
+
end
|
349
|
+
|
350
|
+
def write(key, entries)
|
351
|
+
key = hexdigest(key)
|
352
|
+
cache.set(key, entries)
|
353
|
+
end
|
354
|
+
|
355
|
+
def purge(key)
|
356
|
+
key = hexdigest(key)
|
357
|
+
cache.delete(key)
|
358
|
+
nil
|
359
|
+
rescue Memcached::NotFound
|
360
|
+
nil
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
MEMCACHE =
|
365
|
+
if defined?(::Memcached)
|
366
|
+
MemCached
|
367
|
+
else
|
368
|
+
MemCache
|
369
|
+
end
|
370
|
+
MEMCACHED = MEMCACHE
|
371
|
+
|
372
|
+
class GAEStore < MetaStore
|
373
|
+
attr_reader :cache
|
374
|
+
|
375
|
+
def initialize(options = {})
|
376
|
+
require 'rack/cache/appengine'
|
377
|
+
@cache = Rack::Cache::AppEngine::MemCache.new(options)
|
378
|
+
end
|
379
|
+
|
380
|
+
def read(key)
|
381
|
+
key = hexdigest(key)
|
382
|
+
cache.get(key) || []
|
383
|
+
end
|
384
|
+
|
385
|
+
def write(key, entries)
|
386
|
+
key = hexdigest(key)
|
387
|
+
cache.put(key, entries)
|
388
|
+
end
|
389
|
+
|
390
|
+
def purge(key)
|
391
|
+
key = hexdigest(key)
|
392
|
+
cache.delete(key)
|
393
|
+
nil
|
394
|
+
end
|
395
|
+
|
396
|
+
def self.resolve(uri)
|
397
|
+
self.new(:namespace => uri.host)
|
398
|
+
end
|
399
|
+
|
400
|
+
end
|
401
|
+
|
402
|
+
GAECACHE = GAEStore
|
403
|
+
GAE = GAEStore
|
404
|
+
|
405
|
+
end
|
406
|
+
|
407
|
+
end
|