rack-cache 0.5.2 → 0.5.3

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.

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