rtomayko-rack-cache 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +50 -0
- data/COPYING +18 -0
- data/README +96 -0
- data/Rakefile +144 -0
- data/TODO +42 -0
- data/doc/configuration.markdown +224 -0
- data/doc/events.dot +27 -0
- data/doc/faq.markdown +133 -0
- data/doc/index.markdown +113 -0
- data/doc/layout.html.erb +33 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/storage.markdown +162 -0
- data/lib/rack/cache/config/busters.rb +16 -0
- data/lib/rack/cache/config/default.rb +134 -0
- data/lib/rack/cache/config/no-cache.rb +13 -0
- data/lib/rack/cache/config.rb +65 -0
- data/lib/rack/cache/context.rb +95 -0
- data/lib/rack/cache/core.rb +271 -0
- data/lib/rack/cache/entitystore.rb +224 -0
- data/lib/rack/cache/headers.rb +277 -0
- data/lib/rack/cache/metastore.rb +292 -0
- data/lib/rack/cache/options.rb +119 -0
- data/lib/rack/cache/request.rb +37 -0
- data/lib/rack/cache/response.rb +76 -0
- data/lib/rack/cache/storage.rb +50 -0
- data/lib/rack/cache.rb +51 -0
- data/lib/rack/utils/environment_headers.rb +78 -0
- data/rack-cache.gemspec +74 -0
- data/test/cache_test.rb +35 -0
- data/test/config_test.rb +66 -0
- data/test/context_test.rb +505 -0
- data/test/core_test.rb +84 -0
- data/test/entitystore_test.rb +176 -0
- data/test/environment_headers_test.rb +71 -0
- data/test/headers_test.rb +222 -0
- data/test/logging_test.rb +45 -0
- data/test/metastore_test.rb +210 -0
- data/test/options_test.rb +64 -0
- data/test/pony.jpg +0 -0
- data/test/response_test.rb +37 -0
- data/test/spec_setup.rb +189 -0
- data/test/storage_test.rb +94 -0
- metadata +122 -0
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'rack/utils/environment_headers'
|
3
|
+
|
4
|
+
module Rack::Cache
|
5
|
+
# Generic HTTP header helper methods. Provides access to headers that can be
|
6
|
+
# included in requests and responses. This can be mixed into any object that
|
7
|
+
# responds to #headers by returning a Hash.
|
8
|
+
|
9
|
+
module Headers
|
10
|
+
# Determine if any of the header names exist:
|
11
|
+
# if header?('Authorization', 'Cookie')
|
12
|
+
# ...
|
13
|
+
# end
|
14
|
+
def header?(*names)
|
15
|
+
names.any? { |name| headers.include?(name) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# A Hash of name=value pairs that correspond to the Cache-Control header.
|
19
|
+
# Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
|
20
|
+
# of true. This method always returns a Hash, empty if no Cache-Control
|
21
|
+
# header is present.
|
22
|
+
def cache_control
|
23
|
+
@cache_control ||=
|
24
|
+
(headers['Cache-Control'] || '').split(/\s*,\s*/).inject({}) {|hash,token|
|
25
|
+
name, value = token.split(/\s*=\s*/, 2)
|
26
|
+
hash[name.downcase] = (value || true) unless name.empty?
|
27
|
+
hash
|
28
|
+
}.freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the Cache-Control header to the values specified by the Hash. See
|
32
|
+
# the #cache_control method for information on expected Hash structure.
|
33
|
+
def cache_control=(hash)
|
34
|
+
value =
|
35
|
+
hash.collect { |key,value|
|
36
|
+
next nil unless value
|
37
|
+
next key if value == true
|
38
|
+
"#{key}=#{value}"
|
39
|
+
}.compact.join(', ')
|
40
|
+
if value.empty?
|
41
|
+
headers.delete('Cache-Control')
|
42
|
+
@cache_control = {}
|
43
|
+
else
|
44
|
+
headers['Cache-Control'] = value
|
45
|
+
@cache_control = hash.dup.freeze
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# The literal value of the ETag HTTP header or nil if no ETag is specified.
|
50
|
+
def etag
|
51
|
+
headers['Etag']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# HTTP request header helpers. When included in Rack::Cache::Request, headers
|
56
|
+
# may be accessed by their standard RFC 2616 names using the #headers Hash.
|
57
|
+
module RequestHeaders
|
58
|
+
include Rack::Cache::Headers
|
59
|
+
|
60
|
+
# A Hash-like object providing access to HTTP request headers.
|
61
|
+
def headers
|
62
|
+
@headers ||= Rack::Utils::EnvironmentHeaders.new(env)
|
63
|
+
end
|
64
|
+
|
65
|
+
# The literal value of the If-Modified-Since request header or nil when
|
66
|
+
# no If-Modified-Since header is present.
|
67
|
+
def if_modified_since
|
68
|
+
headers['If-Modified-Since']
|
69
|
+
end
|
70
|
+
|
71
|
+
# The literal value of the If-None-Match request header or nil when
|
72
|
+
# no If-None-Match header is present.
|
73
|
+
def if_none_match
|
74
|
+
headers['If-None-Match']
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# HTTP response header helper methods.
|
79
|
+
module ResponseHeaders
|
80
|
+
include Rack::Cache::Headers
|
81
|
+
|
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
|
95
|
+
|
96
|
+
# Determine if the response is "fresh". Fresh responses may be served from
|
97
|
+
# cache without any interaction with the origin. A response is considered
|
98
|
+
# fresh when it includes a Cache-Control/max-age indicator or Expiration
|
99
|
+
# header and the calculated age is less than the freshness lifetime.
|
100
|
+
def fresh?
|
101
|
+
ttl && ttl > 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# Determine if the response is "stale". Stale responses must be validated
|
105
|
+
# with the origin before use. This is the inverse of #fresh?.
|
106
|
+
def stale?
|
107
|
+
!fresh?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Determine if the response is worth caching under any circumstance. An
|
111
|
+
# object that is cacheable may not necessary be served from cache without
|
112
|
+
# first validating the response with the origin.
|
113
|
+
#
|
114
|
+
# An object that includes no freshness lifetime (Expires, max-age) and that
|
115
|
+
# does not include a validator (Last-Modified, Etag) serves no purpose in a
|
116
|
+
# cache that only serves fresh or valid objects.
|
117
|
+
def cacheable?
|
118
|
+
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
|
119
|
+
return false if no_store?
|
120
|
+
validateable? || fresh?
|
121
|
+
end
|
122
|
+
|
123
|
+
# The response includes specific information about its freshness. True when
|
124
|
+
# a +Cache-Control+ header with +max-age+ value is present or when the
|
125
|
+
# +Expires+ header is set.
|
126
|
+
def freshness_information?
|
127
|
+
header?('Expires') || !cache_control['max-age'].nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Determine if the response includes headers that can be used to validate
|
131
|
+
# the response with the origin using a conditional GET request.
|
132
|
+
def validateable?
|
133
|
+
header?('Last-Modified') || header?('Etag')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Indicates that the response should not be served from cache without first
|
137
|
+
# revalidating with the origin. Note that this does not necessary imply that
|
138
|
+
# a caching agent ought not store the response in its cache.
|
139
|
+
def no_cache?
|
140
|
+
!cache_control['no-cache'].nil?
|
141
|
+
end
|
142
|
+
|
143
|
+
# Indicates that the response should not be stored under any circumstances.
|
144
|
+
def no_store?
|
145
|
+
cache_control['no-store']
|
146
|
+
end
|
147
|
+
|
148
|
+
# The date, as specified by the Date header. When no Date header is present,
|
149
|
+
# set the Date header to Time.now and return.
|
150
|
+
def date
|
151
|
+
if date = headers['Date']
|
152
|
+
Time.httpdate(date)
|
153
|
+
else
|
154
|
+
headers['Date'] = now.httpdate unless headers.frozen?
|
155
|
+
now
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# The age of the response.
|
160
|
+
def age
|
161
|
+
[(now - date).to_i, 0].max
|
162
|
+
end
|
163
|
+
|
164
|
+
# The number of seconds after the time specified in the response's Date
|
165
|
+
# header when the the response should no longer be considered fresh. First
|
166
|
+
# check for a Cache-Control max-age value, and fall back on an expires
|
167
|
+
# header; return nil when no maximum age can be established.
|
168
|
+
def max_age
|
169
|
+
if age = cache_control['max-age']
|
170
|
+
age.to_i
|
171
|
+
elsif headers['Expires']
|
172
|
+
Time.httpdate(headers['Expires']) - date
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Sets the number of seconds after which the response should no longer
|
177
|
+
# be considered fresh. This sets the Cache-Control max-age value.
|
178
|
+
def max_age=(value)
|
179
|
+
self.cache_control = cache_control.merge('max-age' => value.to_s)
|
180
|
+
end
|
181
|
+
|
182
|
+
# The Time when the response should be considered stale. With a
|
183
|
+
# Cache-Control/max-age value is present, this is calculated by adding the
|
184
|
+
# number of seconds specified to the responses #date value. Falls back to
|
185
|
+
# the time specified in the Expires header or returns nil if neither is
|
186
|
+
# present.
|
187
|
+
def expires_at
|
188
|
+
if max_age = cache_control['max-age']
|
189
|
+
date + max_age.to_i
|
190
|
+
elsif time = headers['Expires']
|
191
|
+
Time.httpdate(time)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# The response's time-to-live in seconds, or nil when no freshness
|
196
|
+
# information is present in the response. When the responses #ttl
|
197
|
+
# is <= 0, the response may not be served from cache without first
|
198
|
+
# revalidating with the origin.
|
199
|
+
def ttl
|
200
|
+
max_age - age if max_age
|
201
|
+
end
|
202
|
+
|
203
|
+
# Set the response's time-to-live to the specified number of seconds. This
|
204
|
+
# adjusts the Cache-Control/max-age value.
|
205
|
+
def ttl=(seconds)
|
206
|
+
self.max_age = age + seconds
|
207
|
+
end
|
208
|
+
|
209
|
+
# The String value of the Last-Modified header exactly as it appears
|
210
|
+
# in the response (i.e., no date parsing / conversion is performed).
|
211
|
+
def last_modified
|
212
|
+
headers['Last-Modified']
|
213
|
+
end
|
214
|
+
|
215
|
+
# Determine if the response was last modified at the time provided.
|
216
|
+
# time_value is the exact string provided in an origin response's
|
217
|
+
# Last-Modified header.
|
218
|
+
def last_modified_at?(time_value)
|
219
|
+
time_value && last_modified == time_value
|
220
|
+
end
|
221
|
+
|
222
|
+
# Determine if response's ETag matches the etag value provided. Return
|
223
|
+
# false when either value is nil.
|
224
|
+
def etag_matches?(etag)
|
225
|
+
etag && self.etag == etag
|
226
|
+
end
|
227
|
+
|
228
|
+
# Headers that MUST NOT be included with 304 Not Modified responses.
|
229
|
+
#
|
230
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
231
|
+
NOT_MODIFIED_OMIT_HEADERS = %w[
|
232
|
+
Allow
|
233
|
+
Content-Encoding
|
234
|
+
Content-Language
|
235
|
+
Content-Length
|
236
|
+
Content-Md5
|
237
|
+
Content-Type
|
238
|
+
Last-Modified
|
239
|
+
].to_set
|
240
|
+
|
241
|
+
# Modify the response so that it conforms to the rules defined for
|
242
|
+
# '304 Not Modified'. This sets the status, removes the body, and
|
243
|
+
# discards any headers that MUST NOT be included in 304 responses.
|
244
|
+
#
|
245
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
246
|
+
def not_modified!
|
247
|
+
self.status = 304
|
248
|
+
self.body = []
|
249
|
+
NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
|
253
|
+
# The literal value of the Vary header, or nil when no Vary header is
|
254
|
+
# present.
|
255
|
+
def vary
|
256
|
+
headers['Vary']
|
257
|
+
end
|
258
|
+
|
259
|
+
# Does the response include a Vary header?
|
260
|
+
def vary?
|
261
|
+
! vary.nil?
|
262
|
+
end
|
263
|
+
|
264
|
+
# An array of header names given in the Vary header or an empty
|
265
|
+
# array when no Vary header is present.
|
266
|
+
def vary_header_names
|
267
|
+
return [] unless vary = headers['Vary']
|
268
|
+
vary.split(/[\s,]+/)
|
269
|
+
end
|
270
|
+
|
271
|
+
private
|
272
|
+
def now
|
273
|
+
@now ||= Time.now
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'digest/sha1'
|
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
|
+
entries = read(request.fullpath)
|
29
|
+
|
30
|
+
# bail out if we have nothing cached
|
31
|
+
return nil if entries.empty?
|
32
|
+
|
33
|
+
# find a cached entry that matches the request.
|
34
|
+
env = request.env
|
35
|
+
match = entries.detect{ |req,res| requests_match?(res['Vary'], env, req)}
|
36
|
+
if match
|
37
|
+
# TODO what if body doesn't exist in entity store?
|
38
|
+
# reconstruct response object
|
39
|
+
req, res = match
|
40
|
+
status = res['X-Status']
|
41
|
+
body = entity_store.open(res['X-Content-Digest'])
|
42
|
+
response = Rack::Cache::Response.new(status.to_i, res, body)
|
43
|
+
response.activate!
|
44
|
+
|
45
|
+
# Return the cached response
|
46
|
+
response
|
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
|
+
#--
|
54
|
+
# TODO canonicalize URL key
|
55
|
+
def store(request, response, entity_store)
|
56
|
+
key = request.fullpath
|
57
|
+
stored_env = persist_request(request)
|
58
|
+
|
59
|
+
# write the response body to the entity store if this is the
|
60
|
+
# original response.
|
61
|
+
response['X-Status'] = response.status.to_s
|
62
|
+
if response['X-Content-Digest'].nil?
|
63
|
+
digest, size = entity_store.write(response.body)
|
64
|
+
response['X-Content-Digest'] = digest
|
65
|
+
response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
|
66
|
+
response.body = entity_store.open(digest)
|
67
|
+
response.activate!
|
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
|
+
entries.unshift [stored_env, response.headers.dup]
|
79
|
+
write key, entries
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
# Extract the environment Hash from +request+ while making any
|
84
|
+
# necessary modifications in preparation for persistence. The Hash
|
85
|
+
# returned must be marshalable.
|
86
|
+
def persist_request(request)
|
87
|
+
env = request.env.dup
|
88
|
+
env.reject! { |key,val| key =~ /[^0-9A-Z_]/ }
|
89
|
+
env
|
90
|
+
end
|
91
|
+
|
92
|
+
# Determine whether the two environment hashes are non-varying based on
|
93
|
+
# the vary response header value provided.
|
94
|
+
def requests_match?(vary, env1, env2)
|
95
|
+
return true if vary.nil? || vary == ''
|
96
|
+
vary.split(/[\s,]+/).all? do |header|
|
97
|
+
key = "HTTP_#{header.upcase.tr('-', '_')}"
|
98
|
+
env1[key] == env2[key]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
# Locate all cached request/response pairs that match the specified
|
104
|
+
# URL key. The result must be an Array of all cached request/response
|
105
|
+
# pairs. An empty Array must be returned if nothing is cached for
|
106
|
+
# the specified key.
|
107
|
+
def read(key)
|
108
|
+
raise NotImplemented
|
109
|
+
end
|
110
|
+
|
111
|
+
# Store an Array of request/response pairs for the given key. Concrete
|
112
|
+
# implementations should not attempt to filter or concatenate the
|
113
|
+
# list in any way.
|
114
|
+
def write(key, negotiations)
|
115
|
+
raise NotImplemented
|
116
|
+
end
|
117
|
+
|
118
|
+
# Remove all cached entries at the key specified. No error is raised
|
119
|
+
# when the key does not exist.
|
120
|
+
def purge(key)
|
121
|
+
raise NotImplemented
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Generate a SHA1 hex digest for the specified string. This is a
|
127
|
+
# simple utility method for meta store implementations.
|
128
|
+
def hexdigest(data)
|
129
|
+
Digest::SHA1.hexdigest(data)
|
130
|
+
end
|
131
|
+
|
132
|
+
public
|
133
|
+
|
134
|
+
# Concrete MetaStore implementation that uses a simple Hash to store
|
135
|
+
# request/response pairs on the heap.
|
136
|
+
class Heap < MetaStore
|
137
|
+
def initialize(hash={})
|
138
|
+
@hash = hash
|
139
|
+
end
|
140
|
+
|
141
|
+
def read(key)
|
142
|
+
@hash.fetch(key, []).collect do |req,res|
|
143
|
+
[req.dup, res.dup]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def write(key, entries)
|
148
|
+
@hash[key] = entries
|
149
|
+
end
|
150
|
+
|
151
|
+
def purge(key)
|
152
|
+
@hash.delete(key)
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_hash
|
157
|
+
@hash
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.resolve(uri)
|
161
|
+
new
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
HEAP = Heap
|
166
|
+
MEM = HEAP
|
167
|
+
|
168
|
+
# Concrete MetaStore implementation that stores request/response
|
169
|
+
# pairs on disk.
|
170
|
+
class Disk < MetaStore
|
171
|
+
attr_reader :root
|
172
|
+
|
173
|
+
def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
|
174
|
+
@root = File.expand_path(root)
|
175
|
+
FileUtils.mkdir_p(root, :mode => 0755)
|
176
|
+
end
|
177
|
+
|
178
|
+
def read(key)
|
179
|
+
path = key_path(key)
|
180
|
+
File.open(path, 'rb') { |io| Marshal.load(io) }
|
181
|
+
rescue Errno::ENOENT
|
182
|
+
[]
|
183
|
+
end
|
184
|
+
|
185
|
+
def write(key, entries)
|
186
|
+
path = key_path(key)
|
187
|
+
File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
|
188
|
+
rescue Errno::ENOENT
|
189
|
+
Dir.mkdir(File.dirname(path), 0755)
|
190
|
+
retry
|
191
|
+
end
|
192
|
+
|
193
|
+
def purge(key)
|
194
|
+
path = key_path(key)
|
195
|
+
File.unlink(path)
|
196
|
+
nil
|
197
|
+
rescue Errno::ENOENT
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
def key_path(key)
|
203
|
+
File.join(root, spread(hexdigest(key)))
|
204
|
+
end
|
205
|
+
|
206
|
+
def spread(sha, n=2)
|
207
|
+
sha = sha.dup
|
208
|
+
sha[n,0] = '/'
|
209
|
+
sha
|
210
|
+
end
|
211
|
+
|
212
|
+
public
|
213
|
+
def self.resolve(uri)
|
214
|
+
path = File.expand_path(uri.opaque || uri.path)
|
215
|
+
new path
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
DISK = Disk
|
221
|
+
FILE = Disk
|
222
|
+
|
223
|
+
# Stores request/response pairs in memcached. Keys are not stored
|
224
|
+
# directly since memcached has a 250-byte limit on key names. Instead,
|
225
|
+
# the SHA1 hexdigest of the key is used.
|
226
|
+
class MemCache < MetaStore
|
227
|
+
|
228
|
+
# The Memcached instance used to communicated with the memcached
|
229
|
+
# daemon.
|
230
|
+
attr_reader :cache
|
231
|
+
|
232
|
+
def initialize(server="localhost:11211", options={})
|
233
|
+
@cache =
|
234
|
+
if server.respond_to?(:stats)
|
235
|
+
server
|
236
|
+
else
|
237
|
+
require 'memcached'
|
238
|
+
Memcached.new(server, options)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def read(key)
|
243
|
+
key = hexdigest(key)
|
244
|
+
cache.get(key)
|
245
|
+
rescue Memcached::NotFound
|
246
|
+
[]
|
247
|
+
end
|
248
|
+
|
249
|
+
def write(key, entries)
|
250
|
+
key = hexdigest(key)
|
251
|
+
cache.set(key, entries)
|
252
|
+
end
|
253
|
+
|
254
|
+
def purge(key)
|
255
|
+
key = hexdigest(key)
|
256
|
+
cache.delete(key)
|
257
|
+
nil
|
258
|
+
rescue Memcached::NotFound
|
259
|
+
nil
|
260
|
+
end
|
261
|
+
|
262
|
+
extend Rack::Utils
|
263
|
+
|
264
|
+
# Create MemCache store for the given URI. The URI must specify
|
265
|
+
# a host and may specify a port, namespace, and options:
|
266
|
+
#
|
267
|
+
# memcached://example.com:11211/namespace?opt1=val1&opt2=val2
|
268
|
+
#
|
269
|
+
# Query parameter names and values are documented with the memcached
|
270
|
+
# library: http://tinyurl.com/4upqnd
|
271
|
+
def self.resolve(uri)
|
272
|
+
server = "#{uri.host}:#{uri.port || '11211'}"
|
273
|
+
options = parse_query(uri.query)
|
274
|
+
options.keys.each do |key|
|
275
|
+
value =
|
276
|
+
case value = options.delete(key)
|
277
|
+
when 'true' ; true
|
278
|
+
when 'false' ; false
|
279
|
+
else value.to_sym
|
280
|
+
end
|
281
|
+
options[k.to_sym] = value
|
282
|
+
end
|
283
|
+
options[:namespace] = uri.path.sub(/^\//, '')
|
284
|
+
new server, options
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
MEMCACHE = MemCache
|
289
|
+
MEMCACHED = MemCache
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'rack/cache/storage'
|
3
|
+
|
4
|
+
module Rack::Cache
|
5
|
+
# Configuration options and utility methods for option access. Rack::Cache
|
6
|
+
# uses the Rack Environment to store option values. All options documented
|
7
|
+
# below are stored in the Rack Environment as "rack-cache.<option>", where
|
8
|
+
# <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
|
+
module Options
|
16
|
+
class << self
|
17
|
+
private
|
18
|
+
def option_accessor(key)
|
19
|
+
define_method(key) { || read_option(key) }
|
20
|
+
define_method("#{key}=") { |value| write_option(key, value) }
|
21
|
+
define_method("#{key}?") { || !! read_option(key) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Enable verbose trace logging. This option is currently enabled by
|
26
|
+
# default but is likely to be disabled in a future release.
|
27
|
+
option_accessor :verbose
|
28
|
+
|
29
|
+
# The storage resolver. Defaults to the Rack::Cache.storage singleton instance
|
30
|
+
# of Rack::Cache::Storage. This object is responsible for resolving metastore
|
31
|
+
# and entitystore URIs to an implementation instances.
|
32
|
+
option_accessor :storage
|
33
|
+
|
34
|
+
# A URI specifying the meta-store implementation that should be used to store
|
35
|
+
# request/response meta information. The following URIs schemes are
|
36
|
+
# supported:
|
37
|
+
#
|
38
|
+
# * heap:/
|
39
|
+
# * file:/absolute/path or file:relative/path
|
40
|
+
# * memcached://localhost:11211[/namespace]
|
41
|
+
#
|
42
|
+
# If no meta store is specified the 'heap:/' store is assumed. This
|
43
|
+
# implementation has significant draw-backs so explicit configuration is
|
44
|
+
# recommended.
|
45
|
+
option_accessor :metastore
|
46
|
+
|
47
|
+
# A URI specifying the entity-store implement that should be used to store
|
48
|
+
# response bodies. See the metastore option for information on supported URI
|
49
|
+
# schemes.
|
50
|
+
#
|
51
|
+
# If no entity store is specified the 'heap:/' store is assumed. This
|
52
|
+
# implementation has significant draw-backs so explicit configuration is
|
53
|
+
# recommended.
|
54
|
+
option_accessor :entitystore
|
55
|
+
|
56
|
+
# The number of seconds that a cache entry should be considered
|
57
|
+
# "fresh" when no explicit freshness information is provided in
|
58
|
+
# a response. Explicit Cache-Control or Expires headers
|
59
|
+
# override this value.
|
60
|
+
#
|
61
|
+
# Default: 0
|
62
|
+
option_accessor :default_ttl
|
63
|
+
|
64
|
+
# The underlying options Hash. During initialization (or outside of a
|
65
|
+
# request), this is a default values Hash. During a request, this is the
|
66
|
+
# Rack environment Hash. The default values Hash is merged in underneath
|
67
|
+
# the Rack environment before each request is processed.
|
68
|
+
def options
|
69
|
+
@env || @default_options
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set multiple options.
|
73
|
+
def options=(hash={})
|
74
|
+
hash.each { |key,value| write_option(key, value) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set an option. When +option+ is a Symbol, it is set in the Rack
|
78
|
+
# Environment as "rack-cache.option". When +option+ is a String, it
|
79
|
+
# exactly as specified. The +option+ argument may also be a Hash in
|
80
|
+
# which case each key/value pair is merged into the environment as if
|
81
|
+
# the #set method were called on each.
|
82
|
+
def set(option, value=self)
|
83
|
+
if value == self
|
84
|
+
self.options = option.to_hash
|
85
|
+
else
|
86
|
+
write_option option, value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def read_option(key)
|
92
|
+
options[option_name(key)]
|
93
|
+
end
|
94
|
+
|
95
|
+
def write_option(key, value)
|
96
|
+
options[option_name(key)] = value
|
97
|
+
end
|
98
|
+
|
99
|
+
def option_name(key)
|
100
|
+
case key
|
101
|
+
when Symbol ; "rack-cache.#{key}"
|
102
|
+
when String ; key
|
103
|
+
else raise ArgumentError
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def initialize_options(options={})
|
109
|
+
@default_options = {
|
110
|
+
'rack-cache.verbose' => true,
|
111
|
+
'rack-cache.storage' => Rack::Cache::Storage.instance,
|
112
|
+
'rack-cache.metastore' => 'heap:/',
|
113
|
+
'rack-cache.entitystore' => 'heap:/',
|
114
|
+
'rack-cache.default_ttl' => 0
|
115
|
+
}
|
116
|
+
self.options = options
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|