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 +25 -37
- data/doc/index.markdown +2 -2
- data/example/sinatra/app.rb +0 -4
- data/lib/rack/cache/context.rb +35 -19
- data/lib/rack/cache/metastore.rb +10 -7
- data/lib/rack/cache/response.rb +1 -14
- data/rack-cache.gemspec +4 -2
- data/test/context_test.rb +91 -0
- data/test/response_test.rb +6 -0
- metadata +40 -9
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
|
-
|
90
|
-
|
91
|
-
|
83
|
+
if defined?(Gem)
|
84
|
+
# load gemspec
|
85
|
+
$spec = eval(File.read('rack-cache.gemspec'))
|
92
86
|
|
93
|
-
|
94
|
-
|
87
|
+
def package(ext='')
|
88
|
+
"dist/rack-cache-#{$spec.version}" + ext
|
89
|
+
end
|
95
90
|
|
96
|
-
desc 'Build
|
97
|
-
task :
|
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
|
-
|
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
|
-
|
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('.
|
109
|
-
|
110
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
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
|
122
|
-
|
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|
|
data/doc/index.markdown
CHANGED
@@ -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
|
data/example/sinatra/app.rb
CHANGED
data/lib/rack/cache/context.rb
CHANGED
@@ -55,7 +55,8 @@ module Rack::Cache
|
|
55
55
|
# the context of the receiver.
|
56
56
|
def call!(env)
|
57
57
|
@trace = []
|
58
|
-
@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
|
-
|
107
|
-
|
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
|
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
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
|
data/lib/rack/cache/metastore.rb
CHANGED
@@ -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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
|
data/lib/rack/cache/response.rb
CHANGED
@@ -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
|
data/rack-cache.gemspec
CHANGED
@@ -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.
|
7
|
-
s.date = '
|
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"]
|
data/test/context_test.rb
CHANGED
@@ -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
|
|
data/test/response_test.rb
CHANGED
@@ -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
|
-
|
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:
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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.
|
136
|
+
rubygems_version: 1.3.7
|
106
137
|
signing_key:
|
107
138
|
specification_version: 2
|
108
139
|
summary: HTTP Caching for Rack
|