rack-cache 0.2.0 → 0.3.0

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.

Potentially problematic release.


This version of rack-cache might be problematic. Click here for more details.

@@ -58,6 +58,12 @@ module Rack::Cache
58
58
  [key, size]
59
59
  end
60
60
 
61
+ # Remove the body corresponding to key; return nil.
62
+ def purge(key)
63
+ @hash.delete(key)
64
+ nil
65
+ end
66
+
61
67
  def self.resolve(uri)
62
68
  new
63
69
  end
@@ -89,16 +95,19 @@ module Rack::Cache
89
95
  nil
90
96
  end
91
97
 
92
- # Open the entity body and return an IO object. The IO object's
93
- # each method is overridden to read 8K chunks instead of lines.
94
- def open(key)
95
- io = File.open(body_path(key), 'rb')
96
- def io.each
98
+ class Body < ::File #:nodoc:
99
+ def each
97
100
  while part = read(8192)
98
101
  yield part
99
102
  end
100
103
  end
101
- io
104
+ alias_method :to_path, :path
105
+ end
106
+
107
+ # Open the entity body and return an IO object. The IO object's
108
+ # each method is overridden to read 8K chunks instead of lines.
109
+ def open(key)
110
+ Body.open(body_path(key), 'rb')
102
111
  rescue Errno::ENOENT
103
112
  nil
104
113
  end
@@ -121,6 +130,13 @@ module Rack::Cache
121
130
  [key, size]
122
131
  end
123
132
 
133
+ def purge(key)
134
+ File.unlink body_path(key)
135
+ nil
136
+ rescue Errno::ENOENT
137
+ nil
138
+ end
139
+
124
140
  protected
125
141
  def storage_path(stem)
126
142
  File.join root, stem
@@ -190,6 +206,13 @@ module Rack::Cache
190
206
  [key, size]
191
207
  end
192
208
 
209
+ def purge(key)
210
+ cache.delete(key)
211
+ nil
212
+ rescue Memcached::NotFound
213
+ nil
214
+ end
215
+
193
216
  extend Rack::Utils
194
217
 
195
218
  # Create MemCache store for the given URI. The URI must specify
@@ -21,7 +21,7 @@ module Rack::Cache
21
21
  # header is present.
22
22
  def cache_control
23
23
  @cache_control ||=
24
- (headers['Cache-Control'] || '').split(/\s*,\s*/).inject({}) {|hash,token|
24
+ headers['Cache-Control'].to_s.split(/\s*[,;]\s*/).inject({}) {|hash,token|
25
25
  name, value = token.split(/\s*=\s*/, 2)
26
26
  hash[name.downcase] = (value || true) unless name.empty?
27
27
  hash
@@ -79,9 +79,19 @@ module Rack::Cache
79
79
  module ResponseHeaders
80
80
  include Rack::Cache::Headers
81
81
 
82
- # Set of HTTP response codes of messages that can be cached, per
83
- # RFC 2616.
84
- CACHEABLE_RESPONSE_CODES = Set.new([200, 203, 300, 301, 302, 404, 410])
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
85
95
 
86
96
  # Determine if the response is "fresh". Fresh responses may be served from
87
97
  # cache without any interaction with the origin. A response is considered
@@ -97,16 +107,15 @@ module Rack::Cache
97
107
  !fresh?
98
108
  end
99
109
 
100
- # Determine if the response is worth caching under any circumstance. An
101
- # object that is cacheable may not necessary be served from cache without
102
- # first validating the response with the origin.
110
+ # Determine if the response is worth caching under any circumstance. Responses
111
+ # marked "private" with an explicit Cache-Control directive are considered
112
+ # uncacheable
103
113
  #
104
- # An object that includes no freshness lifetime (Expires, max-age) and that
105
- # does not include a validator (Last-Modified, Etag) serves no purpose in a
106
- # cache that only serves fresh or valid objects.
114
+ # Responses with neither a freshness lifetime (Expires, max-age) nor cache
115
+ # validator (Last-Modified, Etag) are considered uncacheable.
107
116
  def cacheable?
108
117
  return false unless CACHEABLE_RESPONSE_CODES.include?(status)
109
- return false if no_store?
118
+ return false if no_store? || private?
110
119
  validateable? || fresh?
111
120
  end
112
121
 
@@ -114,7 +123,8 @@ module Rack::Cache
114
123
  # a +Cache-Control+ header with +max-age+ value is present or when the
115
124
  # +Expires+ header is set.
116
125
  def freshness_information?
117
- header?('Expires') || !cache_control['max-age'].nil?
126
+ header?('Expires') ||
127
+ !!(cache_control['s-maxage'] || cache_control['max-age'])
118
128
  end
119
129
 
120
130
  # Determine if the response includes headers that can be used to validate
@@ -127,7 +137,7 @@ module Rack::Cache
127
137
  # revalidating with the origin. Note that this does not necessary imply that
128
138
  # a caching agent ought not store the response in its cache.
129
139
  def no_cache?
130
- !cache_control['no-cache'].nil?
140
+ cache_control['no-cache']
131
141
  end
132
142
 
133
143
  # Indicates that the response should not be stored under any circumstances.
@@ -135,16 +145,51 @@ module Rack::Cache
135
145
  cache_control['no-store']
136
146
  end
137
147
 
148
+ # True when the response has been explicitly marked "public".
149
+ def public?
150
+ cache_control['public']
151
+ end
152
+
153
+ # Mark the response "public", making it eligible for other clients. Note
154
+ # that responses are considered "public" by default unless the request
155
+ # includes private headers (Authorization, Cookie).
156
+ def public=(value)
157
+ value = value ? true : nil
158
+ self.cache_control = cache_control.
159
+ merge('public' => value, 'private' => !value)
160
+ end
161
+
162
+ # True when the response has been marked "private" explicitly.
163
+ def private?
164
+ cache_control['private']
165
+ end
166
+
167
+ # Mark the response "private", making it ineligible for serving other
168
+ # clients.
169
+ def private=(value)
170
+ value = value ? true : nil
171
+ self.cache_control = cache_control.
172
+ merge('public' => !value, 'private' => value)
173
+ end
174
+
175
+ # Indicates that the cache must not serve a stale response in any
176
+ # circumstance without first revalidating with the origin. When present,
177
+ # the TTL of the response should not be overriden to be greater than the
178
+ # value provided by the origin.
179
+ def must_revalidate?
180
+ cache_control['must-revalidate'] ||
181
+ cache_control['proxy-revalidate']
182
+ end
183
+
138
184
  # The date, as specified by the Date header. When no Date header is present,
139
185
  # set the Date header to Time.now and return.
140
186
  def date
141
- @date ||=
142
- if date = headers['Date']
143
- Time.httpdate(date)
144
- else
145
- headers['Date'] = now.httpdate unless headers.frozen?
146
- now
147
- end
187
+ if date = headers['Date']
188
+ Time.httpdate(date)
189
+ else
190
+ headers['Date'] = now.httpdate unless headers.frozen?
191
+ now
192
+ end
148
193
  end
149
194
 
150
195
  # The age of the response.
@@ -154,29 +199,36 @@ module Rack::Cache
154
199
 
155
200
  # The number of seconds after the time specified in the response's Date
156
201
  # header when the the response should no longer be considered fresh. First
157
- # check for a Cache-Control max-age value, and fall back on an expires
158
- # header; return nil when no maximum age can be established.
202
+ # check for a s-maxage directive, then a max-age directive, and then fall
203
+ # back on an expires header; return nil when no maximum age can be
204
+ # established.
159
205
  def max_age
160
- if age = cache_control['max-age']
206
+ if age = (cache_control['s-maxage'] || cache_control['max-age'])
161
207
  age.to_i
162
208
  elsif headers['Expires']
163
209
  Time.httpdate(headers['Expires']) - date
164
210
  end
165
211
  end
166
212
 
167
- # Sets the number of seconds after which the response should no longer
168
- # be considered fresh. This sets the Cache-Control max-age value.
213
+ # The number of seconds after which the response should no longer
214
+ # be considered fresh. Sets the Cache-Control max-age directive.
169
215
  def max_age=(value)
170
216
  self.cache_control = cache_control.merge('max-age' => value.to_s)
171
217
  end
172
218
 
219
+ # Like #max_age= but sets the s-maxage directive, which applies only
220
+ # to shared caches.
221
+ def shared_max_age=(value)
222
+ self.cache_control = cache_control.merge('s-maxage' => value.to_s)
223
+ end
224
+
173
225
  # The Time when the response should be considered stale. With a
174
226
  # Cache-Control/max-age value is present, this is calculated by adding the
175
227
  # number of seconds specified to the responses #date value. Falls back to
176
228
  # the time specified in the Expires header or returns nil if neither is
177
229
  # present.
178
230
  def expires_at
179
- if max_age = cache_control['max-age']
231
+ if max_age = (cache_control['s-maxage'] || cache_control['max-age'])
180
232
  date + max_age.to_i
181
233
  elsif time = headers['Expires']
182
234
  Time.httpdate(time)
@@ -191,9 +243,15 @@ module Rack::Cache
191
243
  max_age - age if max_age
192
244
  end
193
245
 
194
- # Set the response's time-to-live to the specified number of seconds. This
195
- # adjusts the Cache-Control/max-age value.
246
+ # Set the response's time-to-live for shared caches to the specified number
247
+ # of seconds. This adjusts the Cache-Control/s-maxage directive.
196
248
  def ttl=(seconds)
249
+ self.shared_max_age = age + seconds
250
+ end
251
+
252
+ # Set the response's time-to-live for private/client caches. This adjusts
253
+ # the Cache-Control/max-age directive.
254
+ def client_ttl=(seconds)
197
255
  self.max_age = age + seconds
198
256
  end
199
257
 
@@ -210,8 +268,38 @@ module Rack::Cache
210
268
  time_value && last_modified == time_value
211
269
  end
212
270
 
213
- # The literal value of the Vary header, or nil when no Vary header is
214
- # present.
271
+ # Determine if response's ETag matches the etag value provided. Return
272
+ # false when either value is nil.
273
+ def etag_matches?(etag)
274
+ etag && self.etag == etag
275
+ end
276
+
277
+ # Headers that MUST NOT be included with 304 Not Modified responses.
278
+ #
279
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
280
+ NOT_MODIFIED_OMIT_HEADERS = %w[
281
+ Allow
282
+ Content-Encoding
283
+ Content-Language
284
+ Content-Length
285
+ Content-Md5
286
+ Content-Type
287
+ Last-Modified
288
+ ].to_set
289
+
290
+ # Modify the response so that it conforms to the rules defined for
291
+ # '304 Not Modified'. This sets the status, removes the body, and
292
+ # discards any headers that MUST NOT be included in 304 responses.
293
+ #
294
+ # http://tools.ietf.org/html/rfc2616#section-10.3.5
295
+ def not_modified!
296
+ self.status = 304
297
+ self.body = []
298
+ NOT_MODIFIED_OMIT_HEADERS.each { |name| headers.delete(name) }
299
+ nil
300
+ end
301
+
302
+ # The literal value of the Vary header, or nil when no header is present.
215
303
  def vary
216
304
  headers['Vary']
217
305
  end
@@ -21,18 +21,6 @@ module Rack::Cache
21
21
  # methods dumb and straight-forward to implement.
22
22
  class MetaStore
23
23
 
24
- # Headers that should not be stored in cache (from RFC 2616).
25
- HEADER_BLACKLIST = Set.new(%w[
26
- Connection
27
- Keep-Alive
28
- Proxy-Authenticate
29
- Proxy-Authorization
30
- TE
31
- Trailers
32
- Transfer-Encoding
33
- Upgrade
34
- ])
35
-
36
24
  # Locate a cached response for the request provided. Returns a
37
25
  # Rack::Cache::Response object if the cache hits or nil if no cache entry
38
26
  # was found.
@@ -44,18 +32,18 @@ module Rack::Cache
44
32
 
45
33
  # find a cached entry that matches the request.
46
34
  env = request.env
47
- match = entries.detect{ |req,res| requests_match?(res['Vary'], env, req)}
48
- if match
49
- # TODO what if body doesn't exist in entity store?
50
- # reconstruct response object
51
- req, res = match
52
- status = res['X-Status']
53
- body = entity_store.open(res['X-Content-Digest'])
54
- response = Rack::Cache::Response.new(status.to_i, res, body)
55
- response.activate!
35
+ match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
36
+ return nil if match.nil?
56
37
 
57
- # Return the cached response
38
+ req, res = match
39
+ if body = entity_store.open(res['X-Content-Digest'])
40
+ response = Rack::Cache::Response.new(res['X-Status'].to_i, res, body)
41
+ response.activate!
58
42
  response
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.
59
47
  end
60
48
  end
61
49
 
@@ -67,25 +55,27 @@ module Rack::Cache
67
55
  def store(request, response, entity_store)
68
56
  key = request.fullpath
69
57
  stored_env = persist_request(request)
70
- stored_response = persist_response(response)
71
58
 
72
59
  # write the response body to the entity store if this is the
73
60
  # original response.
74
- if stored_response['X-Content-Digest'].nil?
61
+ response['X-Status'] = response.status.to_s
62
+ if response['X-Content-Digest'].nil?
75
63
  digest, size = entity_store.write(response.body)
76
- stored_response['X-Content-Digest'] = digest
77
- stored_response['Content-Length'] = size.to_s
64
+ response['X-Content-Digest'] = digest
65
+ response['Content-Length'] = size.to_s unless response['Transfer-Encoding']
78
66
  response.body = entity_store.open(digest)
67
+ response.activate!
79
68
  end
80
69
 
81
70
  # read existing cache entries, remove non-varying, and add this one to
82
71
  # the list
83
- vary = stored_response['Vary']
72
+ vary = response.vary
84
73
  entries =
85
74
  read(key).reject do |env,res|
86
- (vary == res['Vary']) && requests_match?(vary, env, stored_env)
75
+ (vary == res['Vary']) &&
76
+ requests_match?(vary, env, stored_env)
87
77
  end
88
- entries.unshift [stored_env, stored_response]
78
+ entries.unshift [stored_env, {}.update(response.headers)]
89
79
  write key, entries
90
80
  end
91
81
 
@@ -99,15 +89,6 @@ module Rack::Cache
99
89
  env
100
90
  end
101
91
 
102
- # Extract the headers Hash from +response+ while making any
103
- # necessary modifications in preparation for persistence. The Hash
104
- # returned must be marshalable.
105
- def persist_response(response)
106
- headers = response.headers.reject { |k,v| HEADER_BLACKLIST.include?(k) }
107
- headers['X-Status'] = response.status.to_s
108
- headers
109
- end
110
-
111
92
  # Determine whether the two environment hashes are non-varying based on
112
93
  # the vary response header value provided.
113
94
  def requests_match?(vary, env1, env2)
@@ -158,7 +139,9 @@ module Rack::Cache
158
139
  end
159
140
 
160
141
  def read(key)
161
- @hash.fetch(key, [])
142
+ @hash.fetch(key, []).collect do |req,res|
143
+ [req.dup, res.dup]
144
+ end
162
145
  end
163
146
 
164
147
  def write(key, entries)
@@ -61,6 +61,15 @@ module Rack::Cache
61
61
  # Default: 0
62
62
  option_accessor :default_ttl
63
63
 
64
+ # Set of request headers that trigger "private" cache-control behavior
65
+ # on responses that don't explicitly state whether the response is
66
+ # public or private via a Cache-Control directive. Applications that use
67
+ # cookies for authorization may need to add the 'Cookie' header to this
68
+ # list.
69
+ #
70
+ # Default: ['Authorization', 'Cookie']
71
+ option_accessor :private_headers
72
+
64
73
  # The underlying options Hash. During initialization (or outside of a
65
74
  # request), this is a default values Hash. During a request, this is the
66
75
  # Rack environment Hash. The default values Hash is merged in underneath
@@ -111,7 +120,8 @@ module Rack::Cache
111
120
  'rack-cache.storage' => Rack::Cache::Storage.instance,
112
121
  'rack-cache.metastore' => 'heap:/',
113
122
  'rack-cache.entitystore' => 'heap:/',
114
- 'rack-cache.default_ttl' => 0
123
+ 'rack-cache.default_ttl' => 0,
124
+ 'rack-cache.private_headers' => ['Authorization', 'Cookie']
115
125
  }
116
126
  self.options = options
117
127
  end
@@ -34,7 +34,7 @@ module Rack::Cache
34
34
  # and body.
35
35
  def initialize(status, headers, body)
36
36
  @status = status
37
- @headers = headers
37
+ @headers = Rack::Utils::HeaderHash.new(headers)
38
38
  @body = body
39
39
  @now = Time.now
40
40
  @headers['Date'] ||= now.httpdate
@@ -62,7 +62,7 @@ module Rack::Cache
62
62
 
63
63
  # Return the status, headers, and body in a three-tuple.
64
64
  def to_a
65
- [status, headers, body]
65
+ [status, headers.to_hash, body]
66
66
  end
67
67
 
68
68
  # Freezes