rack-cache 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

data/Rakefile CHANGED
@@ -5,12 +5,6 @@ task :default => :test
5
5
  CLEAN.include %w[coverage/ doc/api tags]
6
6
  CLOBBER.include %w[dist]
7
7
 
8
- # load gemspec like github's gem builder to surface any SAFE issues.
9
- Thread.new do
10
- require 'rubygems/specification'
11
- $spec = eval("$SAFE=3\n#{File.read('rack-cache.gemspec')}")
12
- end.join
13
-
14
8
  # SPECS =====================================================================
15
9
 
16
10
  desc 'Run specs with story style output'
@@ -36,7 +30,7 @@ task :doc => %w[doc:api doc:markdown]
36
30
  # requires the hanna gem:
37
31
  # gem install mislav-hanna --source=http://gems.github.com
38
32
  desc 'Build API documentation (doc/api)'
39
- task 'doc:api' => 'doc/api/index.html'
33
+ task 'doc:api' => 'doc/api/index.html'
40
34
  file 'doc/api/index.html' => FileList['lib/**/*.rb'] do |f|
41
35
  rm_rf 'doc/api'
42
36
  sh((<<-SH).gsub(/[\s\n]+/, ' ').strip)
@@ -86,45 +80,39 @@ end
86
80
 
87
81
  # PACKAGING =================================================================
88
82
 
89
- def package(ext='')
90
- "dist/rack-cache-#{$spec.version}" + ext
91
- end
83
+ if defined?(Gem)
84
+ # load gemspec
85
+ $spec = eval(File.read('rack-cache.gemspec'))
92
86
 
93
- desc 'Build packages'
94
- task :package => %w[.gem .tar.gz].map {|e| package(e)}
87
+ def package(ext='')
88
+ "dist/rack-cache-#{$spec.version}" + ext
89
+ end
95
90
 
96
- desc 'Build and install as local gem'
97
- task :install => package('.gem') do
98
- sh "gem install #{package('.gem')}"
99
- end
91
+ desc 'Build packages'
92
+ task :package => %w[.gem .tar.gz].map {|e| package(e)}
100
93
 
101
- directory 'dist/'
94
+ desc 'Build and install as local gem'
95
+ task :install => package('.gem') do
96
+ sh "gem install #{package('.gem')}"
97
+ end
102
98
 
103
- file package('.gem') => %w[dist/ rack-cache.gemspec] + $spec.files do |f|
104
- sh "gem build rack-cache.gemspec"
105
- mv File.basename(f.name), f.name
106
- end
99
+ directory 'dist/'
107
100
 
108
- file package('.tar.gz') => %w[dist/] + $spec.files do |f|
109
- sh "git archive --format=tar HEAD | gzip > #{f.name}"
110
- end
101
+ file package('.gem') => %w[dist/ rack-cache.gemspec] + $spec.files do |f|
102
+ sh "gem build rack-cache.gemspec"
103
+ mv File.basename(f.name), f.name
104
+ end
111
105
 
112
- desc 'Upload gem and tar.gz distributables to rubyforge'
113
- task 'release:rubyforge' => [package('.gem'), package('.tar.gz')] do |t|
114
- sh <<-SH
115
- rubyforge add_release wink rack-cache #{$spec.version} #{package('.gem')} &&
116
- rubyforge add_file wink rack-cache #{$spec.version} #{package('.tar.gz')}
117
- SH
118
- end
106
+ file package('.tar.gz') => %w[dist/] + $spec.files do |f|
107
+ sh "git archive --format=tar HEAD | gzip > #{f.name}"
108
+ end
119
109
 
120
- desc 'Upload gem to gemcutter.org'
121
- task 'release:gemcutter' => [package('.gem')] do |t|
122
- sh "gem push #{package('.gem')}"
110
+ desc 'Upload gem to gemcutter.org'
111
+ task 'release' => [package('.gem')] do |t|
112
+ sh "gem push #{package('.gem')}"
113
+ end
123
114
  end
124
115
 
125
- desc 'Upload gem to gemcutter and rubyforge'
126
- task 'release' => ['release:gemcutter', 'release:rubyforge']
127
-
128
116
  # GEMSPEC ===================================================================
129
117
 
130
118
  file 'rack-cache.gemspec' => FileList['{lib,test}/**','Rakefile'] do |f|
@@ -42,7 +42,7 @@ simply `require` and `use` as follows:
42
42
 
43
43
  use Rack::Cache,
44
44
  :verbose => true,
45
- :metastore => 'file:/var/cache/rack/meta'
45
+ :metastore => 'file:/var/cache/rack/meta',
46
46
  :entitystore => 'file:/var/cache/rack/body'
47
47
 
48
48
  run app
@@ -74,7 +74,7 @@ More
74
74
  * [RDoc API Documentation](./api/) - Mostly worthless if you just want to use
75
75
  __Rack::Cache__ in your application but mildly insightful if you'd like to
76
76
  get a feel for how the system has been put together; I recommend
77
- [reading the source](http://github.com/rtomayko/rack-cache/master/lib/rack/cache).
77
+ [reading the source](http://github.com/rtomayko/rack-cache/tree/master/lib/rack/cache).
78
78
 
79
79
 
80
80
  See Also
@@ -5,10 +5,6 @@ use Rack::Cache do
5
5
  set :verbose, true
6
6
  set :metastore, 'heap:/'
7
7
  set :entitystore, 'heap:/'
8
-
9
- on :receive do
10
- pass! if request.url =~ /favicon/
11
- end
12
8
  end
13
9
 
14
10
  before do
@@ -55,7 +55,8 @@ module Rack::Cache
55
55
  # the context of the receiver.
56
56
  def call!(env)
57
57
  @trace = []
58
- @env = @default_options.merge(env)
58
+ @default_options.each { |k,v| env[k] ||= v }
59
+ @env = env
59
60
  @request = Request.new(@env.dup.freeze)
60
61
 
61
62
  response =
@@ -103,8 +104,13 @@ module Rack::Cache
103
104
  # Determine if the #response validators (ETag, Last-Modified) matches
104
105
  # a conditional value specified in #request.
105
106
  def not_modified?(response)
106
- response.etag_matches?(@request.env['HTTP_IF_NONE_MATCH']) ||
107
- response.last_modified_at?(@request.env['HTTP_IF_MODIFIED_SINCE'])
107
+ last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
108
+ if etags = @request.env['HTTP_IF_NONE_MATCH']
109
+ etags = etags.split(/\s*,\s*/)
110
+ (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
111
+ elsif last_modified
112
+ response.last_modified == last_modified
113
+ end
108
114
  end
109
115
 
110
116
  # Whether the cache entry is "fresh enough" to satisfy the request.
@@ -180,26 +186,36 @@ module Rack::Cache
180
186
  # send no head requests because we want content
181
187
  @env['REQUEST_METHOD'] = 'GET'
182
188
 
183
- # add our cached validators to the environment
189
+ # add our cached last-modified validator to the environment
184
190
  @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
185
- @env['HTTP_IF_NONE_MATCH'] = entry.etag
186
191
 
187
- backend_response = forward
192
+ # Add our cached etag validator to the environment.
193
+ # We keep the etags from the client to handle the case when the client
194
+ # has a different private valid entry which is not cached here.
195
+ cached_etags = entry.etag.to_s.split(/\s*,\s*/)
196
+ request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
197
+ etags = (cached_etags + request_etags).uniq
198
+ @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')
188
199
 
189
- response =
190
- if backend_response.status == 304
191
- record :valid
192
- entry = entry.dup
193
- entry.headers.delete('Date')
194
- %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
195
- next unless value = backend_response.headers[name]
196
- entry.headers[name] = value
197
- end
198
- entry
199
- else
200
- record :invalid
201
- backend_response
200
+ response = forward
201
+
202
+ if response.status == 304
203
+ record :valid
204
+
205
+ # Check if the response validated which is not cached here
206
+ etag = response.headers['ETag']
207
+ return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)
208
+
209
+ entry = entry.dup
210
+ entry.headers.delete('Date')
211
+ %w[Date Expires Cache-Control ETag Last-Modified].each do |name|
212
+ next unless value = response.headers[name]
213
+ entry.headers[name] = value
202
214
  end
215
+ response = entry
216
+ else
217
+ record :invalid
218
+ end
203
219
 
204
220
  store(response) if response.cacheable?
205
221
 
@@ -214,23 +214,26 @@ module Rack::Cache
214
214
  def read(key)
215
215
  path = key_path(key)
216
216
  File.open(path, 'rb') { |io| Marshal.load(io) }
217
- rescue Errno::ENOENT
217
+ rescue Errno::ENOENT, IOError
218
218
  []
219
219
  end
220
220
 
221
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
222
+ tries = 0
223
+ begin
224
+ path = key_path(key)
225
+ File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
226
+ rescue Errno::ENOENT, IOError
227
+ Dir.mkdir(File.dirname(path), 0755)
228
+ retry if (tries += 1) == 1
229
+ end
227
230
  end
228
231
 
229
232
  def purge(key)
230
233
  path = key_path(key)
231
234
  File.unlink(path)
232
235
  nil
233
- rescue Errno::ENOENT
236
+ rescue Errno::ENOENT, IOError
234
237
  nil
235
238
  end
236
239
 
@@ -30,7 +30,7 @@ module Rack::Cache
30
30
  # Create a Response instance given the response status code, header hash,
31
31
  # and body.
32
32
  def initialize(status, headers, body)
33
- @status = status
33
+ @status = status.to_i
34
34
  @headers = Rack::Utils::HeaderHash.new(headers)
35
35
  @body = body
36
36
  @now = Time.now
@@ -208,19 +208,6 @@ module Rack::Cache
208
208
  headers['ETag']
209
209
  end
210
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
211
  # Headers that MUST NOT be included with 304 Not Modified responses.
225
212
  #
226
213
  # http://tools.ietf.org/html/rfc2616#section-10.3.5
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
 
5
5
  s.name = 'rack-cache'
6
- s.version = '0.5.2'
7
- s.date = '2009-09-25'
6
+ s.version = '0.5.3'
7
+ s.date = '2010-09-10'
8
8
 
9
9
  s.description = "HTTP Caching for Rack"
10
10
  s.summary = "HTTP Caching for Rack"
@@ -61,6 +61,8 @@ Gem::Specification.new do |s|
61
61
  s.extra_rdoc_files = %w[README COPYING TODO CHANGES]
62
62
  s.add_dependency 'rack', '>= 0.4'
63
63
 
64
+ s.add_development_dependency 'test-spec'
65
+
64
66
  s.has_rdoc = true
65
67
  s.homepage = "http://tomayko.com/src/rack-cache/"
66
68
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Rack::Cache", "--main", "Rack::Cache"]
@@ -115,6 +115,97 @@ describe 'Rack::Cache::Context' do
115
115
  cache.trace.should.include :store
116
116
  end
117
117
 
118
+ it 'responds with 304 only if If-None-Match and If-Modified-Since both match' do
119
+ timestamp = Time.now
120
+
121
+ respond_with do |req,res|
122
+ res.status = 200
123
+ res['ETag'] = '12345'
124
+ res['Last-Modified'] = timestamp.httpdate
125
+ res['Content-Type'] = 'text/plain'
126
+ res.body = ['Hello World']
127
+ end
128
+
129
+ # Only etag matches
130
+ get '/',
131
+ 'HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => (timestamp - 1).httpdate
132
+ app.should.be.called
133
+ response.status.should.equal 200
134
+
135
+ # Only last-modified matches
136
+ get '/',
137
+ 'HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => timestamp.httpdate
138
+ app.should.be.called
139
+ response.status.should.equal 200
140
+
141
+ # Both matches
142
+ get '/',
143
+ 'HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => timestamp.httpdate
144
+ app.should.be.called
145
+ response.status.should.equal 304
146
+ end
147
+
148
+ it 'validates private responses cached on the client' do
149
+ respond_with do |req,res|
150
+ etags = req.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
151
+ if req.env['HTTP_COOKIE'] == 'authenticated'
152
+ res['Cache-Control'] = 'private, no-store'
153
+ res['ETag'] = '"private tag"'
154
+ if etags.include?('"private tag"')
155
+ res.status = 304
156
+ else
157
+ res.status = 200
158
+ res['Content-Type'] = 'text/plain'
159
+ res.body = ['private data']
160
+ end
161
+ else
162
+ res['ETag'] = '"public tag"'
163
+ if etags.include?('"public tag"')
164
+ res.status = 304
165
+ else
166
+ res.status = 200
167
+ res['Content-Type'] = 'text/plain'
168
+ res.body = ['public data']
169
+ end
170
+ end
171
+ end
172
+
173
+ get '/'
174
+ app.should.be.called
175
+ response.status.should.equal 200
176
+ response.headers['ETag'].should == '"public tag"'
177
+ response.body.should == 'public data'
178
+ cache.trace.should.include :miss
179
+ cache.trace.should.include :store
180
+
181
+ get '/', 'HTTP_COOKIE' => 'authenticated'
182
+ app.should.be.called
183
+ response.status.should.equal 200
184
+ response.headers['ETag'].should == '"private tag"'
185
+ response.body.should == 'private data'
186
+ cache.trace.should.include :stale
187
+ cache.trace.should.include :invalid
188
+ cache.trace.should.not.include :store
189
+
190
+ get '/',
191
+ 'HTTP_IF_NONE_MATCH' => '"public tag"'
192
+ app.should.be.called
193
+ response.status.should.equal 304
194
+ response.headers['ETag'].should == '"public tag"'
195
+ cache.trace.should.include :stale
196
+ cache.trace.should.include :valid
197
+ cache.trace.should.include :store
198
+
199
+ get '/',
200
+ 'HTTP_IF_NONE_MATCH' => '"private tag"',
201
+ 'HTTP_COOKIE' => 'authenticated'
202
+ app.should.be.called
203
+ response.status.should.equal 304
204
+ response.headers['ETag'].should == '"private tag"'
205
+ cache.trace.should.include :valid
206
+ cache.trace.should.not.include :store
207
+ end
208
+
118
209
  it 'stores responses when no-cache request directive present' do
119
210
  respond_with 200, 'Expires' => (Time.now + 5).httpdate
120
211
 
@@ -12,6 +12,12 @@ describe 'Rack::Cache::Response' do
12
12
  @now, @res, @one_hour_ago = nil
13
13
  end
14
14
 
15
+ it 'marks Rack tuples with string typed statuses as cacheable' do
16
+ @res = Rack::Cache::Response.new('200',{'Date' => @now.httpdate},[])
17
+ @res.headers['Expires'] = @one_hour_later.httpdate
18
+ @res.cacheable?.should.equal true
19
+ end
20
+
15
21
  it 'responds to #to_a with a Rack response tuple' do
16
22
  @res.should.respond_to :to_a
17
23
  @res.to_a.should.equal [200, {'Date' => @now.httpdate}, []]
metadata CHANGED
@@ -1,7 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ hash: 13
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 5
9
+ - 3
10
+ version: 0.5.3
5
11
  platform: ruby
6
12
  authors:
7
13
  - Ryan Tomayko
@@ -9,19 +15,38 @@ autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2009-09-25 00:00:00 -07:00
18
+ date: 2010-09-10 00:00:00 -07:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
22
  name: rack
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ - 4
23
33
  version: "0.4"
24
- version:
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: test-spec
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :development
49
+ version_requirements: *id002
25
50
  description: HTTP Caching for Rack
26
51
  email: r@tomayko.com
27
52
  executables: []
@@ -88,21 +113,27 @@ rdoc_options:
88
113
  require_paths:
89
114
  - lib
90
115
  required_ruby_version: !ruby/object:Gem::Requirement
116
+ none: false
91
117
  requirements:
92
118
  - - ">="
93
119
  - !ruby/object:Gem::Version
120
+ hash: 3
121
+ segments:
122
+ - 0
94
123
  version: "0"
95
- version:
96
124
  required_rubygems_version: !ruby/object:Gem::Requirement
125
+ none: false
97
126
  requirements:
98
127
  - - ">="
99
128
  - !ruby/object:Gem::Version
129
+ hash: 3
130
+ segments:
131
+ - 0
100
132
  version: "0"
101
- version:
102
133
  requirements: []
103
134
 
104
135
  rubyforge_project: wink
105
- rubygems_version: 1.3.4
136
+ rubygems_version: 1.3.7
106
137
  signing_key:
107
138
  specification_version: 2
108
139
  summary: HTTP Caching for Rack