josh-rack-cache 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,150 @@
|
|
1
|
+
require 'rack/cache/key'
|
2
|
+
require 'rack/cache/storage'
|
3
|
+
|
4
|
+
module Rack::Cache
|
5
|
+
|
6
|
+
# Configuration options and utility methods for option access. Rack::Cache
|
7
|
+
# uses the Rack Environment to store option values. All options documented
|
8
|
+
# below are stored in the Rack Environment as "rack-cache.<option>", where
|
9
|
+
# <option> is the option name.
|
10
|
+
module Options
|
11
|
+
def self.option_accessor(key)
|
12
|
+
name = option_name(key)
|
13
|
+
define_method(key) { || options[name] }
|
14
|
+
define_method("#{key}=") { |value| options[name] = value }
|
15
|
+
define_method("#{key}?") { || !! options[name] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def option_name(key)
|
19
|
+
case key
|
20
|
+
when Symbol ; "rack-cache.#{key}"
|
21
|
+
when String ; key
|
22
|
+
else raise ArgumentError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
module_function :option_name
|
26
|
+
|
27
|
+
# Enable verbose trace logging. This option is currently enabled by
|
28
|
+
# default but is likely to be disabled in a future release.
|
29
|
+
option_accessor :verbose
|
30
|
+
|
31
|
+
# The storage resolver. Defaults to the Rack::Cache.storage singleton instance
|
32
|
+
# of Rack::Cache::Storage. This object is responsible for resolving metastore
|
33
|
+
# and entitystore URIs to an implementation instances.
|
34
|
+
option_accessor :storage
|
35
|
+
|
36
|
+
# A URI specifying the meta-store implementation that should be used to store
|
37
|
+
# request/response meta information. The following URIs schemes are
|
38
|
+
# supported:
|
39
|
+
#
|
40
|
+
# * heap:/
|
41
|
+
# * file:/absolute/path or file:relative/path
|
42
|
+
# * memcached://localhost:11211[/namespace]
|
43
|
+
#
|
44
|
+
# If no meta store is specified the 'heap:/' store is assumed. This
|
45
|
+
# implementation has significant draw-backs so explicit configuration is
|
46
|
+
# recommended.
|
47
|
+
option_accessor :metastore
|
48
|
+
|
49
|
+
# A custom cache key generator, which can be anything that responds to :call.
|
50
|
+
# By default, this is the Rack::Cache::Key class, but you can implement your
|
51
|
+
# own generator. A cache key generator gets passed a request and generates the
|
52
|
+
# appropriate cache key.
|
53
|
+
#
|
54
|
+
# In addition to setting the generator to an object, you can just pass a block
|
55
|
+
# instead, which will act as the cache key generator:
|
56
|
+
#
|
57
|
+
# set :cache_key do |request|
|
58
|
+
# request.fullpath.replace(/\//, '-')
|
59
|
+
# end
|
60
|
+
option_accessor :cache_key
|
61
|
+
|
62
|
+
# A URI specifying the entity-store implementation that should be used to
|
63
|
+
# store response bodies. See the metastore option for information on
|
64
|
+
# supported URI schemes.
|
65
|
+
#
|
66
|
+
# If no entity store is specified the 'heap:/' store is assumed. This
|
67
|
+
# implementation has significant draw-backs so explicit configuration is
|
68
|
+
# recommended.
|
69
|
+
option_accessor :entitystore
|
70
|
+
|
71
|
+
# The number of seconds that a cache entry should be considered
|
72
|
+
# "fresh" when no explicit freshness information is provided in
|
73
|
+
# a response. Explicit Cache-Control or Expires headers
|
74
|
+
# override this value.
|
75
|
+
#
|
76
|
+
# Default: 0
|
77
|
+
option_accessor :default_ttl
|
78
|
+
|
79
|
+
# Set of request headers that trigger "private" cache-control behavior
|
80
|
+
# on responses that don't explicitly state whether the response is
|
81
|
+
# public or private via a Cache-Control directive. Applications that use
|
82
|
+
# cookies for authorization may need to add the 'Cookie' header to this
|
83
|
+
# list.
|
84
|
+
#
|
85
|
+
# Default: ['Authorization', 'Cookie']
|
86
|
+
option_accessor :private_headers
|
87
|
+
|
88
|
+
# Specifies whether the client can force a cache reload by including a
|
89
|
+
# Cache-Control "no-cache" directive in the request. This is enabled by
|
90
|
+
# default for compliance with RFC 2616.
|
91
|
+
option_accessor :allow_reload
|
92
|
+
|
93
|
+
# Specifies whether the client can force a cache revalidate by including
|
94
|
+
# a Cache-Control "max-age=0" directive in the request. This is enabled by
|
95
|
+
# default for compliance with RFC 2616.
|
96
|
+
option_accessor :allow_revalidate
|
97
|
+
|
98
|
+
# The underlying options Hash. During initialization (or outside of a
|
99
|
+
# request), this is a default values Hash. During a request, this is the
|
100
|
+
# Rack environment Hash. The default values Hash is merged in underneath
|
101
|
+
# the Rack environment before each request is processed.
|
102
|
+
def options
|
103
|
+
@env || @default_options
|
104
|
+
end
|
105
|
+
|
106
|
+
# Set multiple options.
|
107
|
+
def options=(hash={})
|
108
|
+
hash.each { |key,value| write_option(key, value) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set an option. When +option+ is a Symbol, it is set in the Rack
|
112
|
+
# Environment as "rack-cache.option". When +option+ is a String, it
|
113
|
+
# exactly as specified. The +option+ argument may also be a Hash in
|
114
|
+
# which case each key/value pair is merged into the environment as if
|
115
|
+
# the #set method were called on each.
|
116
|
+
def set(option, value=self, &block)
|
117
|
+
if block_given?
|
118
|
+
write_option option, block
|
119
|
+
elsif value == self
|
120
|
+
self.options = option.to_hash
|
121
|
+
else
|
122
|
+
write_option option, value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def initialize_options(options={})
|
128
|
+
@default_options = {
|
129
|
+
'rack-cache.cache_key' => Key,
|
130
|
+
'rack-cache.verbose' => true,
|
131
|
+
'rack-cache.storage' => Rack::Cache::Storage.instance,
|
132
|
+
'rack-cache.metastore' => 'heap:/',
|
133
|
+
'rack-cache.entitystore' => 'heap:/',
|
134
|
+
'rack-cache.default_ttl' => 0,
|
135
|
+
'rack-cache.private_headers' => ['Authorization', 'Cookie'],
|
136
|
+
'rack-cache.allow_reload' => false,
|
137
|
+
'rack-cache.allow_revalidate' => false
|
138
|
+
}
|
139
|
+
self.options = options
|
140
|
+
end
|
141
|
+
|
142
|
+
def read_option(key)
|
143
|
+
options[option_name(key)]
|
144
|
+
end
|
145
|
+
|
146
|
+
def write_option(key, value)
|
147
|
+
options[option_name(key)] = value
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
require 'rack/cache/cachecontrol'
|
3
|
+
|
4
|
+
module Rack::Cache
|
5
|
+
|
6
|
+
# Provides access to the HTTP request. The +request+ and +original_request+
|
7
|
+
# objects exposed by the Core caching engine are instances of this class.
|
8
|
+
#
|
9
|
+
# Request objects respond to a variety of convenience methods, including
|
10
|
+
# everything defined by Rack::Request as well as the Headers and
|
11
|
+
# RequestHeaders modules.
|
12
|
+
class Request < Rack::Request
|
13
|
+
# The HTTP request method. This is the standard implementation of this
|
14
|
+
# method but is respecified here due to libraries that attempt to modify
|
15
|
+
# the behavior to respect POST tunnel method specifiers. We always want
|
16
|
+
# the real request method.
|
17
|
+
def request_method
|
18
|
+
@env['REQUEST_METHOD']
|
19
|
+
end
|
20
|
+
|
21
|
+
# A CacheControl instance based on the request's Cache-Control header.
|
22
|
+
def cache_control
|
23
|
+
@cache_control ||= CacheControl.new(env['HTTP_CACHE_CONTROL'])
|
24
|
+
end
|
25
|
+
|
26
|
+
# True when the Cache-Control/no-cache directive is present or the
|
27
|
+
# Pragma header is set to no-cache.
|
28
|
+
def no_cache?
|
29
|
+
cache_control['no-cache'] ||
|
30
|
+
env['HTTP_PRAGMA'] == 'no-cache'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'set'
|
3
|
+
require 'rack/response'
|
4
|
+
require 'rack/utils'
|
5
|
+
require 'rack/cache/cachecontrol'
|
6
|
+
|
7
|
+
module Rack::Cache
|
8
|
+
|
9
|
+
# Provides access to the response generated by the downstream application. The
|
10
|
+
# +response+, +original_response+, and +entry+ objects exposed by the Core
|
11
|
+
# caching engine are instances of this class.
|
12
|
+
#
|
13
|
+
# Response objects respond to a variety of convenience methods, including
|
14
|
+
# those defined in Rack::Response::Helpers, Rack::Cache::Headers,
|
15
|
+
# and Rack::Cache::ResponseHeaders.
|
16
|
+
#
|
17
|
+
# Note that Rack::Cache::Response is not a subclass of Rack::Response and does
|
18
|
+
# not perform many of the same initialization and finalization tasks. For
|
19
|
+
# example, the body is not slurped during initialization and there are no
|
20
|
+
# facilities for generating response output.
|
21
|
+
class Response
|
22
|
+
include Rack::Response::Helpers
|
23
|
+
|
24
|
+
# Rack response tuple accessors.
|
25
|
+
attr_accessor :status, :headers, :body
|
26
|
+
|
27
|
+
# The time when the Response object was instantiated.
|
28
|
+
attr_reader :now
|
29
|
+
|
30
|
+
# Create a Response instance given the response status code, header hash,
|
31
|
+
# and body.
|
32
|
+
def initialize(status, headers, body)
|
33
|
+
@status = status
|
34
|
+
@headers = Rack::Utils::HeaderHash.new(headers)
|
35
|
+
@body = body
|
36
|
+
@now = Time.now
|
37
|
+
@headers['Date'] ||= @now.httpdate
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize_copy(other)
|
41
|
+
super
|
42
|
+
@headers = other.headers.dup
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the status, headers, and body in a three-tuple.
|
46
|
+
def to_a
|
47
|
+
[status, headers.to_hash, body]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Status codes of responses that MAY be stored by a cache or used in reply
|
51
|
+
# to a subsequent request.
|
52
|
+
#
|
53
|
+
# http://tools.ietf.org/html/rfc2616#section-13.4
|
54
|
+
CACHEABLE_RESPONSE_CODES = [
|
55
|
+
200, # OK
|
56
|
+
203, # Non-Authoritative Information
|
57
|
+
300, # Multiple Choices
|
58
|
+
301, # Moved Permanently
|
59
|
+
302, # Found
|
60
|
+
404, # Not Found
|
61
|
+
410 # Gone
|
62
|
+
].to_set
|
63
|
+
|
64
|
+
# A Hash of name=value pairs that correspond to the Cache-Control header.
|
65
|
+
# Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
|
66
|
+
# of true. This method always returns a Hash, empty if no Cache-Control
|
67
|
+
# header is present.
|
68
|
+
def cache_control
|
69
|
+
@cache_control ||= CacheControl.new(headers['Cache-Control'])
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the Cache-Control header to the values specified by the Hash. See
|
73
|
+
# the #cache_control method for information on expected Hash structure.
|
74
|
+
def cache_control=(value)
|
75
|
+
if value.respond_to? :to_hash
|
76
|
+
cache_control.clear
|
77
|
+
cache_control.merge!(value)
|
78
|
+
value = cache_control.to_s
|
79
|
+
end
|
80
|
+
|
81
|
+
if value.nil? || value.empty?
|
82
|
+
headers.delete('Cache-Control')
|
83
|
+
else
|
84
|
+
headers['Cache-Control'] = value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Determine if the response is "fresh". Fresh responses may be served from
|
89
|
+
# cache without any interaction with the origin. A response is considered
|
90
|
+
# fresh when it includes a Cache-Control/max-age indicator or Expiration
|
91
|
+
# header and the calculated age is less than the freshness lifetime.
|
92
|
+
def fresh?
|
93
|
+
ttl && ttl > 0
|
94
|
+
end
|
95
|
+
|
96
|
+
# Determine if the response is worth caching under any circumstance. Responses
|
97
|
+
# marked "private" with an explicit Cache-Control directive are considered
|
98
|
+
# uncacheable
|
99
|
+
#
|
100
|
+
# Responses with neither a freshness lifetime (Expires, max-age) nor cache
|
101
|
+
# validator (Last-Modified, ETag) are considered uncacheable.
|
102
|
+
def cacheable?
|
103
|
+
return false unless CACHEABLE_RESPONSE_CODES.include?(status)
|
104
|
+
return false if cache_control.no_store? || cache_control.private?
|
105
|
+
validateable? || fresh?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Determine if the response includes headers that can be used to validate
|
109
|
+
# the response with the origin using a conditional GET request.
|
110
|
+
def validateable?
|
111
|
+
headers.key?('Last-Modified') || headers.key?('ETag')
|
112
|
+
end
|
113
|
+
|
114
|
+
# Mark the response "private", making it ineligible for serving other
|
115
|
+
# clients.
|
116
|
+
def private=(value)
|
117
|
+
value = value ? true : nil
|
118
|
+
self.cache_control = cache_control.
|
119
|
+
merge('public' => !value, 'private' => value)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Indicates that the cache must not serve a stale response in any
|
123
|
+
# circumstance without first revalidating with the origin. When present,
|
124
|
+
# the TTL of the response should not be overriden to be greater than the
|
125
|
+
# value provided by the origin.
|
126
|
+
def must_revalidate?
|
127
|
+
cache_control.must_revalidate || cache_control.proxy_revalidate
|
128
|
+
end
|
129
|
+
|
130
|
+
# Mark the response stale by setting the Age header to be equal to the
|
131
|
+
# maximum age of the response.
|
132
|
+
def expire!
|
133
|
+
headers['Age'] = max_age.to_s if fresh?
|
134
|
+
end
|
135
|
+
|
136
|
+
# The date, as specified by the Date header. When no Date header is present,
|
137
|
+
# set the Date header to Time.now and return.
|
138
|
+
def date
|
139
|
+
if date = headers['Date']
|
140
|
+
Time.httpdate(date)
|
141
|
+
else
|
142
|
+
headers['Date'] = now.httpdate unless headers.frozen?
|
143
|
+
now
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# The age of the response.
|
148
|
+
def age
|
149
|
+
(headers['Age'] || [(now - date).to_i, 0].max).to_i
|
150
|
+
end
|
151
|
+
|
152
|
+
# The number of seconds after the time specified in the response's Date
|
153
|
+
# header when the the response should no longer be considered fresh. First
|
154
|
+
# check for a s-maxage directive, then a max-age directive, and then fall
|
155
|
+
# back on an expires header; return nil when no maximum age can be
|
156
|
+
# established.
|
157
|
+
def max_age
|
158
|
+
cache_control.shared_max_age ||
|
159
|
+
cache_control.max_age ||
|
160
|
+
(expires && (expires - date))
|
161
|
+
end
|
162
|
+
|
163
|
+
# The value of the Expires header as a Time object.
|
164
|
+
def expires
|
165
|
+
headers['Expires'] && Time.httpdate(headers['Expires'])
|
166
|
+
end
|
167
|
+
|
168
|
+
# The number of seconds after which the response should no longer
|
169
|
+
# be considered fresh. Sets the Cache-Control max-age directive.
|
170
|
+
def max_age=(value)
|
171
|
+
self.cache_control = cache_control.merge('max-age' => value.to_s)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Like #max_age= but sets the s-maxage directive, which applies only
|
175
|
+
# to shared caches.
|
176
|
+
def shared_max_age=(value)
|
177
|
+
self.cache_control = cache_control.merge('s-maxage' => value.to_s)
|
178
|
+
end
|
179
|
+
|
180
|
+
# The response's time-to-live in seconds, or nil when no freshness
|
181
|
+
# information is present in the response. When the responses #ttl
|
182
|
+
# is <= 0, the response may not be served from cache without first
|
183
|
+
# revalidating with the origin.
|
184
|
+
def ttl
|
185
|
+
max_age - age if max_age
|
186
|
+
end
|
187
|
+
|
188
|
+
# Set the response's time-to-live for shared caches to the specified number
|
189
|
+
# of seconds. This adjusts the Cache-Control/s-maxage directive.
|
190
|
+
def ttl=(seconds)
|
191
|
+
self.shared_max_age = age + seconds
|
192
|
+
end
|
193
|
+
|
194
|
+
# Set the response's time-to-live for private/client caches. This adjusts
|
195
|
+
# the Cache-Control/max-age directive.
|
196
|
+
def client_ttl=(seconds)
|
197
|
+
self.max_age = age + seconds
|
198
|
+
end
|
199
|
+
|
200
|
+
# The String value of the Last-Modified header exactly as it appears
|
201
|
+
# in the response (i.e., no date parsing / conversion is performed).
|
202
|
+
def last_modified
|
203
|
+
headers['Last-Modified']
|
204
|
+
end
|
205
|
+
|
206
|
+
# The literal value of ETag HTTP header or nil if no ETag is specified.
|
207
|
+
def etag
|
208
|
+
headers['ETag']
|
209
|
+
end
|
210
|
+
|
211
|
+
# Determine if the response was last modified at the time provided.
|
212
|
+
# time_value is the exact string provided in an origin response's
|
213
|
+
# Last-Modified header.
|
214
|
+
def last_modified_at?(time_value)
|
215
|
+
time_value && last_modified == time_value
|
216
|
+
end
|
217
|
+
|
218
|
+
# Determine if response's ETag matches the etag value provided. Return
|
219
|
+
# false when either value is nil.
|
220
|
+
def etag_matches?(etag)
|
221
|
+
etag && self.etag == etag
|
222
|
+
end
|
223
|
+
|
224
|
+
# Headers that MUST NOT be included with 304 Not Modified responses.
|
225
|
+
#
|
226
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
227
|
+
NOT_MODIFIED_OMIT_HEADERS = %w[
|
228
|
+
Allow
|
229
|
+
Content-Encoding
|
230
|
+
Content-Language
|
231
|
+
Content-Length
|
232
|
+
Content-MD5
|
233
|
+
Content-Type
|
234
|
+
Last-Modified
|
235
|
+
].to_set
|
236
|
+
|
237
|
+
# Modify the response so that it conforms to the rules defined for
|
238
|
+
# '304 Not Modified'. This sets the status, removes the body, and
|
239
|
+
# discards any headers that MUST NOT be included in 304 responses.
|
240
|
+
#
|
241
|
+
# http://tools.ietf.org/html/rfc2616#section-10.3.5
|
242
|
+
def not_modified!
|
243
|
+
self.status = 304
|
244
|
+
self.body = []
|
245
|
+
NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
# The literal value of the Vary header, or nil when no header is present.
|
250
|
+
def vary
|
251
|
+
headers['Vary']
|
252
|
+
end
|
253
|
+
|
254
|
+
# Does the response include a Vary header?
|
255
|
+
def vary?
|
256
|
+
! vary.nil?
|
257
|
+
end
|
258
|
+
|
259
|
+
# An array of header names given in the Vary header or an empty
|
260
|
+
# array when no Vary header is present.
|
261
|
+
def vary_header_names
|
262
|
+
return [] unless vary = headers['Vary']
|
263
|
+
vary.split(/[\s,]+/)
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
end
|