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.
@@ -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