├── .github
└── workflows
│ ├── development.yml
│ └── gh-pages.yml
├── .gitignore
├── CHANGES
├── MIT-LICENSE
├── README.md
├── Rakefile
├── doc
├── configuration.markdown
├── faq.markdown
├── index.markdown
├── layout.html.erb
├── license.markdown
├── rack-cache.css
├── server.ru
└── storage.markdown
├── example
└── sinatra
│ ├── app.rb
│ └── views
│ └── index.erb
├── gems.rb
├── gems
├── rack_v2-1.rb
├── rack_v2.rb
└── rack_v3.rb
├── lib
├── rack-cache.rb
└── rack
│ ├── cache.rb
│ └── cache
│ ├── app_engine.rb
│ ├── appengine.rb
│ ├── cache_control.rb
│ ├── cachecontrol.rb
│ ├── context.rb
│ ├── entity_store.rb
│ ├── entitystore.rb
│ ├── headers.rb
│ ├── key.rb
│ ├── meta_store.rb
│ ├── metastore.rb
│ ├── options.rb
│ ├── request.rb
│ ├── response.rb
│ ├── storage.rb
│ └── version.rb
├── rack-cache.gemspec
└── test
├── cache_control_test.rb
├── cache_test.rb
├── context_test.rb
├── entity_store_test.rb
├── key_test.rb
├── meta_store_test.rb
├── options_test.rb
├── pony.jpg
├── request_test.rb
├── response_test.rb
├── storage_test.rb
└── test_helper.rb
/.github/workflows/development.yml:
--------------------------------------------------------------------------------
1 | name: Development
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: ${{matrix.ruby}} on ${{matrix.os}} (${{matrix.gemfile}})
8 | runs-on: ${{matrix.os}}-latest
9 | continue-on-error: ${{matrix.experimental}}
10 |
11 | strategy:
12 | matrix:
13 | os:
14 | - ubuntu
15 | - macos
16 |
17 | ruby:
18 | - "2.7"
19 | - "3.0"
20 | - "3.1"
21 | - "3.2"
22 | - "3.3"
23 |
24 | gemfile:
25 | - gems/rack_v2.rb
26 | - gems/rack_v2-1.rb
27 | - gems/rack_v3.rb
28 |
29 | experimental: [false]
30 |
31 | include:
32 | - os: ubuntu
33 | ruby: truffleruby
34 | experimental: true
35 | - os: ubuntu
36 | ruby: jruby
37 | experimental: true
38 | - os: ubuntu
39 | ruby: head
40 | experimental: true
41 |
42 | env:
43 | BUNDLE_GEMFILE: ${{matrix.gemfile}}
44 |
45 | steps:
46 | - uses: actions/checkout@v3
47 |
48 | - name: Installing packages (ubuntu)
49 | if: matrix.os == 'ubuntu'
50 | run: sudo apt-get install libmemcached-dev
51 |
52 | - name: Installing packages (macos)
53 | if: matrix.os == 'macos'
54 | run: brew install libmemcached
55 |
56 | - uses: ruby/setup-ruby@v1
57 | with:
58 | ruby-version: ${{matrix.ruby}}
59 | bundler-cache: true
60 |
61 | - run: bundle exec rake
62 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Github Pages
2 | on:
3 | push:
4 | branches:
5 | - main
6 | permissions:
7 | contents: write
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: ruby/setup-ruby@v1
14 | with:
15 | ruby-version: '2.7'
16 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
17 |
18 | - name: Install and Build 🔧
19 | run: bundle exec rake doc:gh-pages
20 |
21 | - name: Deploy 🚀
22 | uses: JamesIves/github-pages-deploy-action@v4.3.3
23 | with:
24 | branch: gh-pages # The branch the action should deploy to.
25 | folder: doc/gh-pages # The folder the action should deploy.
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | \[.
2 | tags
3 | .vimrc
4 | /dist
5 | /coverage
6 | /doc/api
7 | /doc/*.png
8 | /doc/*.pdf
9 | /doc/*.svg
10 | /doc/config
11 | /doc/configuration.html
12 | /doc/index.html
13 | /doc/license.html
14 | /doc/storage.html
15 | /doc/faq.html
16 | /doc/gh-pages
17 | gems.locked
18 | /pkg
19 |
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | ## 1.13.0
2 |
3 | * Support lowercase Vary and Age headers
4 |
5 | ## 1.12.0
6 |
7 | * Add a fault_tolerant flag to fail-over to stale cache
8 |
9 | ## 1.11.1
10 |
11 | * when ignoring parts of the query, remove query in key when all params are ignored
12 |
13 | ## 1.11.0
14 |
15 | * Add a proc to allow ignoring parts of the query string in the key
16 |
17 | ## 1.10.0
18 |
19 | * Pass Options To Underlying Storage Driver
20 | * bump required ruby version to 2.3
21 |
22 | ## 1.9.0
23 |
24 | * make purge not raise when not implemented
25 |
26 | ## 1.8.0
27 |
28 | * Meta stores will purge keys when no entity store entries are found
29 |
30 | ## 1.7.2
31 |
32 | * Fix key generation for requests with no query strings
33 |
34 | ## 1.7.0
35 |
36 | * Meta stores now receive a third ttl argument to write when use_native_ttl is used.
37 |
38 | ## 1.6.1
39 |
40 | * Revert 'No longer read responses from cache when we already have them'
41 |
42 | ## 1.6.0
43 |
44 | * Noop backend
45 | * No longer read responses from cache when we already have them
46 | * renamed files from entitystore -> entity_store (metastore/cachecontrol/appengine) and added warns for old ones
47 |
48 | ## 1.5.1
49 |
50 | * fix key generation for query strings that include encoded equals
51 |
52 | ## 1.5.0
53 |
54 | * only catch StandardError and not Exception
55 |
56 | ## 1.4.3
57 |
58 | * After overriding the REQUEST_METHOD, store the original request method in "rack.methodoverride.original_method"
59 |
60 | ## 1.4.1
61 |
62 | * Ignore invalid Expires date as per RFC
63 |
64 | ## 1.4.0
65 |
66 | * Not invalidating the cache for preflight CORS request
67 |
68 | ## 1.3.1 / October 2015
69 |
70 | * Support Ruby 1.9
71 |
72 | ## 1.3 / Octorber 2015
73 |
74 | * Ruby 2.0 only
75 |
76 | * Gracefully degrade when cache store goes offline
77 |
78 | * allow_reload/revalidate is not enabled by default
79 |
80 | * Make Rack::Cache multithread friendly
81 |
82 | ## 1.2 / March 2012
83 |
84 | * Fix a cookie leak vulnerability effecting large numbers of Rails 3.x installs:
85 | https://github.com/rtomayko/rack-cache/pull/52
86 |
87 | * Never 304 on PUT or POST requests.
88 |
89 | * Misc bundler and test tooling fixes.
90 |
91 | ## 1.1 / September 2011
92 |
93 | * Allow (INM/IMS) validation requests through to backend on miss. Makes it
94 | possible to use validation for private / uncacheable responses. A number of
95 | people using Rails's stale?() helper reported that their validation logic was
96 | never kicking in.
97 |
98 | * Add rack env rack-cache.force-pass option to bypass rack-cache on
99 | per request basis
100 |
101 | * Fix an issue with memcache namespace not being set when using the
102 | :namespace option instead of :prefix_key.
103 |
104 | * Fix test failures due to MockResponse changes in recent Rack
105 | version (issue #34)
106 |
107 | ## 1.0.3 / August 2011
108 |
109 | * Fix bug passing options to memcached and dalli
110 |
111 | * Document cache_key
112 |
113 | ## 1.0.1 / April 2011
114 |
115 | * Added lib/rack-cache.rb to match package name for auto-requiring machinery.
116 |
117 | * Fixed a number of issues caused by Rack::Cache not closing the body received
118 | from the application. Rack::Lock and other middleware use body.close to
119 | signal the true end of request processing so failure to call this method
120 | can result in strange issues (e.g.,
121 | "ThreadError: deadlock; recursive locking")
122 |
123 | * Fixed a bug where Rack::Cache would blow up writing the rack env to the meta
124 | store when the env contained an all uppercase key whose value wasn't
125 | marshalable. Passenger and some other stuff write such keys apparently.
126 |
127 | * The test suite has moved from test-spec to bacon. This is a short term
128 | solution to the problem of not being able to run tests under Ruby 1.9.x.
129 | The test suite will be moved to basic Test::Unit style sometime in the
130 | future.
131 |
132 | ## 1.0 / December 2010
133 |
134 | * Rack::Cache is 1.0 and will now maintain semantic versioning
135 |
136 | * Add Dalli memcache client support and removed support for the unmaintained
137 | memcache-client library. You will need to move your apps to Dalli before
138 | upgrading rack-cache to 1.0.
139 |
140 | ## 0.5.3 / September 2010
141 |
142 | * A matching If-Modified-Since is ignored if an If-None-Match is also provided
143 | and doesn't match. This is in line with RFC 2616.
144 |
145 | * Converts string status codes to integers before returns to workaround bad
146 | behaving rack middleware and apps.
147 |
148 | * Misc doc clean up.
149 |
150 | ## 0.5.2 / September 2009
151 |
152 | * Exceptions raised from the metastore are not fatal. This makes a lot of
153 | sense in most cases because its okay for the cache to be down - it
154 | shouldn't blow up your app.
155 |
156 | ## 0.5.1 / June 2009
157 |
158 | * Added support for memcached clusters and other advanced
159 | configuration provided by the memcache-client and memcached
160 | libraries. The "metastore" and "entitystore" options can now be
161 | set to a MemCache object or Memcached object:
162 |
163 | memcache = MemCache.new(['127.1.1.1', '127.1.1.2'], :namespace => "/foo")
164 | use Rack::Cache,
165 | :metastore => memcache,
166 | :entitystore => memcache
167 |
168 | * Fix "memcached://" metastore URL handling. The "memcached" variation
169 | blew up, the "memcache" version was fine.
170 |
171 | ## 0.5.0 / May 2009
172 |
173 | * Added meta and entity store implementations based on the
174 | memcache-client library. These are the default unless the memcached
175 | library has already been required.
176 |
177 | * The "allow_reload" and "allow_revalidate" options now default to
178 | false instead of true. This means we break with RFC 2616 out of
179 | the box but this is the expected configuration in a huge majority
180 | of gateway cache scenarios. See the docs on configuration
181 | options for more information on these options:
182 | http://tomayko.com/src/rack-cache/configuration
183 |
184 | * Added Google AppEngine memcache entity store and metastore
185 | implementations. To use GAE's memcache with rack-cache, set the
186 | "metastore" and "entitystore" options as follows:
187 |
188 | use Rack::Cache,
189 | :metastore => 'gae://cache-meta',
190 | :entitystore => 'gae://cache-body'
191 |
192 | The 'cache-meta' and 'cache-body' parts are memcache namespace
193 | prefixes and should be set to different values.
194 |
195 | ## 0.4.0 / March 2009
196 |
197 | * Ruby 1.9.1 / Rack 1.0 compatible.
198 |
199 | * Invalidate cache entries that match the request URL on non-GET/HEAD
200 | requests. i.e., POST, PUT, DELETE cause matching cache entries to
201 | be invalidated. The cache entry is validated with the backend using
202 | a conditional GET the next time it's requested.
203 |
204 | * Implement "Cache-Control: max-age=N" request directive by forcing
205 | validation when the max-age provided exceeds the age of the cache
206 | entry. This can be disabled by setting the "allow_revalidate" option to
207 | false.
208 |
209 | * Properly implement "Cache-Control: no-cache" request directive by
210 | performing a full reload. RFC 2616 states that when "no-cache" is
211 | present in the request, the cache MUST NOT serve a stored response even
212 | after successful validation. This is slightly different from the
213 | "no-cache" directive in responses, which indicates that the cache must
214 | first validate its entry with the origin. Previously, we implemented
215 | "no-cache" on requests by passing so no new cache entry would be stored
216 | based on the response. Now we treat it as a forced miss and enter the
217 | response into the cache if it's cacheable. This can be disabled by
218 | setting the "allow_reload" option to false.
219 |
220 | * Assume identical semantics for the "Pragma: no-cache" request header
221 | as the "Cache-Control: no-cache" directive described above.
222 |
223 | * Less crazy logging. When the verbose option is set, a single log entry
224 | is written with a comma separated list of trace events. For example, if
225 | the cache was stale but validated, the following log entry would be
226 | written: "cache: stale, valid, store". When the verbose option is false,
227 | no logging occurs.
228 |
229 | * Added "X-Rack-Cache" response header with the same comma separated trace
230 | value as described above. This gives some visibility into how the cache
231 | processed the request.
232 |
233 | * Add support for canonicalized cache keys, as well as custom cache key
234 | generators, which are specified in the options as :cache_key as either
235 | any object that has a call() or as a block. Cache key generators get
236 | passed a request object and return a cache key string.
237 |
238 | ## 0.3.0 / December 2008
239 |
240 | * Add support for public and private cache control directives. Responses
241 | marked as explicitly public are cached even when the request includes
242 | an Authorization or Cookie header. Responses marked as explicitly private
243 | are considered uncacheable.
244 |
245 | * Added a "private_headers" option that dictates which request headers
246 | trigger default "private" cache control processing. By default, the
247 | Cookie and Authorization headers are included. Headers may be added or
248 | removed as necessary to change the default private logic.
249 |
250 | * Adhere to must-revalidate/proxy-revalidate cache control directives by
251 | not assigning the default_ttl to responses that don't include freshness
252 | information. This should let us begin using default_ttl more liberally
253 | since we can control it using the must-revalidate/proxy-revalidate directives.
254 |
255 | * Use the s-maxage Cache-Control value in preference to max-age when
256 | present. The ttl= method now sets the s-maxage value instead of max-age.
257 | Code that used ttl= to control freshness at the client needs to change
258 | to set the max-age directive explicitly.
259 |
260 | * Enable support for X-Sendfile middleware by responding to #to_path on
261 | bodies served from disk storage. Adding the Rack::Sendfile component
262 | upstream from Rack::Cache will result in cached bodies being served
263 | directly by the web server (instead of being read in Ruby).
264 |
265 | * BUG: MetaStore hits but EntityStore misses. This would 500 previously; now
266 | we detect it and act as if the MetaStore missed as well.
267 |
268 | * Implement low level #purge method on all concrete entity store
269 | classes -- removes the entity body corresponding to the SHA1 key
270 | provided and returns nil.
271 |
272 | * Basically sane handling of HEAD requests. A HEAD request is never passed
273 | through to the backend except when transitioning with pass!. This means
274 | that the cache responds to HEAD requests without invoking the backend at
275 | all when the cached entry is fresh. When no cache entry exists, or the
276 | cached entry is stale and can be validated, the backend is invoked with
277 | a GET request and the HEAD is handled right before the response
278 | is delivered upstream.
279 |
280 | * BUG: The Age response header was not being set properly when a stale
281 | entry was validated. This would result in Age values that exceeded
282 | the freshness lifetime in responses.
283 |
284 | * BUG: A cached entry in a heap meta store could be unintentionally
285 | modified by request processing since the cached objects were being
286 | returned directly. The result was typically missing/incorrect header
287 | values (e.g., missing Content-Type header). [dkubb]
288 |
289 | * BUG: 304 responses should not include entity headers (especially
290 | Content-Length). This is causing Safari/WebKit weirdness on 304
291 | responses.
292 |
293 | * BUG: The If-None-Match header was being ignored, causing the cache
294 | to send 200 responses to matching conditional GET requests.
295 |
296 | ## 0.2.0 / 2008-10-24 / Initial Release
297 |
298 | * Document events and transitions in `rack/cache/config/default.rb`
299 | * Basic logging support (`trace`, `warn`, `info`, `error` from within Context)
300 | * EntityStore: store entity bodies keyed by SHA
301 | * MetaStore: store response headers keyed by URL
302 | * Last-Modified/ETag validation
303 | * Vary support
304 | * Implement error! transition
305 | * New Rack::Cache::Core
306 | * memcached meta and entity store implementations
307 | * URI based storage configuration
308 | * Read options from Rack env if present (rack-cache.XXX keys)
309 | * `object` is now `entry`
310 | * Documentation framework and website
311 | * Document storage areas and implementations
312 | * Document configuration/events
313 |
314 | ## 0.1.0 / 2008-07-21 / Proof of concept (unreleased)
315 |
316 | * Basic core with event support
317 | * `#import` method for bringing in config files
318 | * Freshness based expiration
319 | * RFC 2616 If-Modified-Since based validation
320 | * A horribly shitty storage back-end (Hash in mem)
321 | * Don't cache hop-by-hop headers: Connection, Keep-Alive, Proxy-Authenticate,
322 | Proxy-Authorization, TE, Trailers, Transfer-Encoding, Upgrade
323 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008 Ryan Tomayko
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Rack::Cache
2 | ===========
3 |
4 | Rack::Cache is suitable as a quick drop-in component to enable HTTP caching for
5 | Rack-based applications that produce freshness (`expires`, `cache-control`)
6 | and/or validation (`last-modified`, `etag`) information:
7 |
8 | * Standards-based (RFC 2616)
9 | * Freshness/expiration based caching
10 | * Validation (`if-modified-since` / `if-none-match`)
11 | * `vary` support
12 | * `cache-control` `public`, `private`, `max-age`, `s-maxage`, `must-revalidate`,
13 | and `proxy-revalidate`.
14 | * Portable: 100% Ruby / works with any Rack-enabled framework
15 | * Disk, memcached, and heap memory storage backends
16 |
17 | For more information about Rack::Cache features and usage, see:
18 |
19 | https://rack.github.io/rack-cache/
20 |
21 | Rack::Cache is not overly optimized for performance. The main goal of the
22 | project is to provide a portable, easy-to-configure, and standards-based
23 | caching solution for small to medium sized deployments. More sophisticated /
24 | high-performance caching systems (e.g., Varnish, Squid, httpd/mod-cache) may be
25 | more appropriate for large deployments with significant throughput requirements.
26 |
27 | Installation
28 | ------------
29 |
30 | gem install rack-cache
31 |
32 | Basic Usage
33 | -----------
34 |
35 | `Rack::Cache` is implemented as a piece of Rack middleware and can be used with
36 | any Rack-based application. If your application includes a rackup (`.ru`) file
37 | or uses Rack::Builder to construct the application pipeline, simply require
38 | and use as follows:
39 |
40 | ```Ruby
41 | require 'rack/cache'
42 |
43 | use Rack::Cache,
44 | metastore: 'file:/var/cache/rack/meta',
45 | entitystore: 'file:/var/cache/rack/body',
46 | verbose: true
47 |
48 | run app
49 | ```
50 |
51 | Assuming you've designed your backend application to take advantage of HTTP's
52 | caching features, no further code or configuration is required for basic
53 | caching.
54 |
55 | Using with Rails
56 | ----------------
57 |
58 | ```Ruby
59 | # config/application.rb
60 | config.action_dispatch.rack_cache = true
61 | # or
62 | config.action_dispatch.rack_cache = {
63 | verbose: true,
64 | metastore: 'file:/var/cache/rack/meta',
65 | entitystore: 'file:/var/cache/rack/body'
66 | }
67 | ```
68 |
69 | You should now see `Rack::Cache` listed in the middleware pipeline:
70 |
71 | rake middleware
72 |
73 | [more information](https://snippets.aktagon.com/snippets/302-how-to-setup-and-use-rack-cache-with-rails)
74 |
75 | Using with Dalli
76 | ----------------
77 |
78 | Dalli is a high performance memcached client for Ruby.
79 | More information at: https://github.com/mperham/dalli
80 |
81 | ```Ruby
82 | require 'dalli'
83 | require 'rack/cache'
84 |
85 | use Rack::Cache,
86 | verbose: true,
87 | metastore: "memcached://localhost:11211/meta",
88 | entitystore: "memcached://localhost:11211/body"
89 |
90 | run app
91 | ```
92 |
93 | Noop entity store
94 | -----------------
95 |
96 | Does not persist response bodies (no disk/memory used).
97 | Responses from the cache will have an empty body.
98 | Clients must ignore these empty cached response (check for `x-rack-cache` response header).
99 | Atm cannot handle streamed responses, patch needed.
100 |
101 | ```Ruby
102 | require 'rack/cache'
103 |
104 | use Rack::Cache,
105 | verbose: true,
106 | metastore:
107 | entitystore: "noop:/"
108 |
109 | run app
110 | ```
111 |
112 | Ignoring tracking parameters in cache keys
113 | -----------------
114 |
115 | It's fairly common to include tracking parameters which don't affect the content
116 | of the page. Since Rack::Cache uses the full URL as part of the cache key, this
117 | can cause unneeded churn in your cache. If you're using the default key class
118 | `Rack::Cache::Key`, you can configure a proc to ignore certain keys/values like
119 | so:
120 |
121 | ```Ruby
122 | Rack::Cache::Key.query_string_ignore = proc { |k, v| k =~ /^(trk|utm)_/ }
123 | ```
124 |
125 | License: MIT
126 | [](https://github.com/rack/rack-cache/actions/workflows/development.yml)
127 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'bundler/gem_tasks'
3 | require 'rake/clean'
4 | require 'bump/tasks'
5 |
6 | task :default => :test
7 |
8 | CLEAN.include %w[coverage/ doc/api doc/gh-pages tags]
9 | CLOBBER.include %w[dist]
10 |
11 | desc 'Run tests'
12 | task :test do
13 | sh "bundle exec mtest test"
14 | end
15 |
16 | desc 'Generate test coverage report'
17 | task :rcov do
18 | sh "rcov -I.:lib:test test/*_test.rb"
19 | end
20 |
21 | # DOC =======================================================================
22 | desc 'Build all documentation'
23 | task :doc => %w[doc:api doc:markdown]
24 |
25 | desc 'Build API documentation (doc/api)'
26 | task 'doc:api' => 'doc/api/index.html'
27 | file 'doc/api/index.html' => FileList['lib/**/*.rb'] do |f|
28 | rm_rf 'doc/api'
29 | sh((<<-SH).gsub(/[\s\n]+/, ' ').strip)
30 | rdoc
31 | --op doc/api
32 | --charset utf8
33 | --fmt hanna
34 | --line-numbers
35 | --main cache.rb
36 | --title 'Rack::Cache API Documentation'
37 | #{f.prerequisites.join(' ')}
38 | SH
39 | end
40 | CLEAN.include 'doc/api'
41 |
42 | desc 'Build markdown documentation files'
43 | task 'doc:markdown'
44 | FileList['doc/*.markdown'].each do |source|
45 | dest = "doc/#{File.basename(source, '.markdown')}.html"
46 | file dest => [source, 'doc/layout.html.erb'] do |f|
47 | puts "markdown: #{source} -> #{dest}" if verbose
48 | require 'erb' unless defined? ERB
49 | template = File.read(source)
50 | content = Markdown.new(ERB.new(template, 0, "%<>").result(binding), :smart).to_html
51 | content.match("
(.*)
")[1] rescue ''
52 | layout = ERB.new(File.read("doc/layout.html.erb"), 0, "%<>")
53 | output = layout.result(binding)
54 | File.open(dest, 'w') { |io| io.write(output) }
55 | end
56 | task 'doc:markdown' => dest
57 | CLEAN.include dest
58 | end
59 |
60 | desc 'Move documentation to directory for github pages'
61 | task 'doc:gh-pages' => [:clean, :doc] do
62 | html_files = FileList['doc/*.markdown'].map { |file| file.gsub('.markdown', '.html')}
63 | css_files = FileList['doc/*.css']
64 |
65 | FileUtils.mkdir('doc/gh-pages')
66 | FileUtils.cp_r('doc/api/', 'doc/gh-pages/api')
67 | FileUtils.cp([*html_files, *css_files], 'doc/gh-pages')
68 | end
69 |
70 | desc 'Start the documentation development server'
71 | task 'doc:server' do
72 | sh 'cd doc && thin --rackup server.ru --port 3035 start'
73 | end
74 |
--------------------------------------------------------------------------------
/doc/configuration.markdown:
--------------------------------------------------------------------------------
1 | Configuration
2 | =============
3 |
4 | __Rack::Cache__ includes a configuration system that can be used to specify
5 | fairly sophisticated cache policy on a global or per-request basis.
6 |
7 |
8 |
9 | Setting Cache Options
10 | ---------------------
11 |
12 | Cache options can be set when the __Rack::Cache__ object is created,
13 | or by setting a `rack-cache.
43 |
44 |
45 |
--------------------------------------------------------------------------------
/gems.rb:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | group :maintenance, optional: true do
6 | gem "bake"
7 | gem "bake-gem"
8 | end
9 |
--------------------------------------------------------------------------------
/gems/rack_v2-1.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec path: "../"
6 |
7 | gem 'rack', '~> 2.1.0'
8 |
--------------------------------------------------------------------------------
/gems/rack_v2.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec path: "../"
6 |
7 | gem 'rack', '~> 2.0'
8 |
--------------------------------------------------------------------------------
/gems/rack_v3.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | gemspec path: "../"
6 |
7 | gem 'rack', '~> 3.0'
8 |
--------------------------------------------------------------------------------
/lib/rack-cache.rb:
--------------------------------------------------------------------------------
1 | require 'rack/cache'
2 |
--------------------------------------------------------------------------------
/lib/rack/cache.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 |
3 | # = HTTP Caching For Rack
4 | #
5 | # Rack::Cache is suitable as a quick, drop-in component to enable HTTP caching
6 | # for Rack-enabled applications that produce freshness (+expires+, +cache-control+)
7 | # and/or validation (+last-modified+, +etag+) information.
8 | #
9 | # * Standards-based (RFC 2616 compliance)
10 | # * Freshness/expiration based caching and validation
11 | # * Supports HTTP Vary
12 | # * Portable: 100% Ruby / works with any Rack-enabled framework
13 | # * Disk, memcached, and heap memory storage backends
14 | #
15 | # === Usage
16 | #
17 | # Create with default options:
18 | # require 'rack/cache'
19 | # Rack::Cache.new(app, :verbose => true, :entitystore => 'file:cache')
20 | #
21 | # Within a rackup file (or with Rack::Builder):
22 | # require 'rack/cache'
23 | # use Rack::Cache do
24 | # set :verbose, true
25 | # set :metastore, 'memcached://localhost:11211/meta'
26 | # set :entitystore, 'file:/var/cache/rack'
27 | # end
28 | # run app
29 | module Rack::Cache
30 | autoload :Request, 'rack/cache/request'
31 | autoload :Response, 'rack/cache/response'
32 | autoload :Context, 'rack/cache/context'
33 | autoload :Storage, 'rack/cache/storage'
34 | autoload :CacheControl, 'rack/cache/cache_control'
35 |
36 | # Create a new Rack::Cache middleware component that fetches resources from
37 | # the specified backend application. The +options+ Hash can be used to
38 | # specify default configuration values (see attributes defined in
39 | # Rack::Cache::Options for possible key/values). When a block is given, it
40 | # is executed within the context of the newly create Rack::Cache::Context
41 | # object.
42 | def self.new(backend, options={}, &b)
43 | Context.new(backend, options, &b)
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/rack/cache/app_engine.rb:
--------------------------------------------------------------------------------
1 | require 'base64'
2 |
3 | module Rack::Cache::AppEngine
4 | module MC
5 | require 'java'
6 |
7 | import com.google.appengine.api.memcache.Expiration;
8 | import com.google.appengine.api.memcache.MemcacheService;
9 | import com.google.appengine.api.memcache.MemcacheServiceFactory;
10 | import com.google.appengine.api.memcache.Stats;
11 |
12 | Service = MemcacheServiceFactory.getMemcacheService
13 | end unless defined?(Rack::Cache::AppEngine::MC)
14 |
15 | class MemCache
16 | def initialize(options = {})
17 | @cache = MC::Service
18 | @cache.namespace = options[:namespace] if options[:namespace]
19 | end
20 |
21 | def contains?(key)
22 | MC::Service.contains(key)
23 | end
24 |
25 | def get(key)
26 | value = MC::Service.get(key)
27 | Marshal.load(Base64.decode64(value)) if value
28 | end
29 |
30 | def put(key, value, ttl = nil)
31 | expiration = ttl ? MC::Expiration.byDeltaSeconds(ttl) : nil
32 | value = Base64.encode64(Marshal.dump(value)).gsub(/\n/, '')
33 | MC::Service.put(key, value, expiration)
34 | end
35 |
36 | def namespace
37 | MC::Service.getNamespace
38 | end
39 |
40 | def namespace=(value)
41 | MC::Service.setNamespace(value.to_s)
42 | end
43 |
44 | def delete(key)
45 | MC::Service.delete(key)
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/rack/cache/appengine.rb:
--------------------------------------------------------------------------------
1 | warn "use require 'rack/cache/app_engine'"
2 | require 'rack/cache/app_engine'
3 |
--------------------------------------------------------------------------------
/lib/rack/cache/cache_control.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | module Cache
3 |
4 | # Parses a cache-control header and exposes the directives as a Hash.
5 | # Directives that do not have values are set to +true+.
6 | class CacheControl < Hash
7 | def initialize(value=nil)
8 | parse(value)
9 | end
10 |
11 | # Indicates that the response MAY be cached by any cache, even if it
12 | # would normally be non-cacheable or cacheable only within a non-
13 | # shared cache.
14 | #
15 | # A response may be considered public without this directive if the
16 | # private directive is not set and the request does not include an
17 | # Authorization header.
18 | def public?
19 | self['public']
20 | end
21 |
22 | # Indicates that all or part of the response message is intended for
23 | # a single user and MUST NOT be cached by a shared cache. This
24 | # allows an origin server to state that the specified parts of the
25 | # response are intended for only one user and are not a valid
26 | # response for requests by other users. A private (non-shared) cache
27 | # MAY cache the response.
28 | #
29 | # Note: This usage of the word private only controls where the
30 | # response may be cached, and cannot ensure the privacy of the
31 | # message content.
32 | def private?
33 | self['private']
34 | end
35 |
36 | # When set in a response, a cache MUST NOT use the response to satisfy a
37 | # subsequent request without successful revalidation with the origin
38 | # server. This allows an origin server to prevent caching even by caches
39 | # that have been configured to return stale responses to client requests.
40 | #
41 | # Note that this does not necessary imply that the response may not be
42 | # stored by the cache, only that the cache cannot serve it without first
43 | # making a conditional GET request with the origin server.
44 | #
45 | # When set in a request, the server MUST NOT use a cached copy for its
46 | # response. This has quite different semantics compared to the no-cache
47 | # directive on responses. When the client specifies no-cache, it causes
48 | # an end-to-end reload, forcing each cache to update their cached copies.
49 | def no_cache?
50 | self['no-cache']
51 | end
52 |
53 | # Indicates that the response MUST NOT be stored under any circumstances.
54 | #
55 | # The purpose of the no-store directive is to prevent the
56 | # inadvertent release or retention of sensitive information (for
57 | # example, on backup tapes). The no-store directive applies to the
58 | # entire message, and MAY be sent either in a response or in a
59 | # request. If sent in a request, a cache MUST NOT store any part of
60 | # either this request or any response to it. If sent in a response,
61 | # a cache MUST NOT store any part of either this response or the
62 | # request that elicited it. This directive applies to both non-
63 | # shared and shared caches. "MUST NOT store" in this context means
64 | # that the cache MUST NOT intentionally store the information in
65 | # non-volatile storage, and MUST make a best-effort attempt to
66 | # remove the information from volatile storage as promptly as
67 | # possible after forwarding it.
68 | #
69 | # The purpose of this directive is to meet the stated requirements
70 | # of certain users and service authors who are concerned about
71 | # accidental releases of information via unanticipated accesses to
72 | # cache data structures. While the use of this directive might
73 | # improve privacy in some cases, we caution that it is NOT in any
74 | # way a reliable or sufficient mechanism for ensuring privacy. In
75 | # particular, malicious or compromised caches might not recognize or
76 | # obey this directive, and communications networks might be
77 | # vulnerable to eavesdropping.
78 | def no_store?
79 | self['no-store']
80 | end
81 |
82 | # The expiration time of an entity MAY be specified by the origin
83 | # server using the expires header (see section 14.21). Alternatively,
84 | # it MAY be specified using the max-age directive in a response. When
85 | # the max-age cache-control directive is present in a cached response,
86 | # the response is stale if its current age is greater than the age
87 | # value given (in seconds) at the time of a new request for that
88 | # resource. The max-age directive on a response implies that the
89 | # response is cacheable (i.e., "public") unless some other, more
90 | # restrictive cache directive is also present.
91 | #
92 | # If a response includes both an expires header and a max-age
93 | # directive, the max-age directive overrides the expires header, even
94 | # if the expires header is more restrictive. This rule allows an origin
95 | # server to provide, for a given response, a longer expiration time to
96 | # an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be
97 | # useful if certain HTTP/1.0 caches improperly calculate ages or
98 | # expiration times, perhaps due to desynchronized clocks.
99 | #
100 | # Many HTTP/1.0 cache implementations will treat an expires value that
101 | # is less than or equal to the response Date value as being equivalent
102 | # to the cache-control response directive "no-cache". If an HTTP/1.1
103 | # cache receives such a response, and the response does not include a
104 | # cache-control header field, it SHOULD consider the response to be
105 | # non-cacheable in order to retain compatibility with HTTP/1.0 servers.
106 | #
107 | # When the max-age directive is included in the request, it indicates
108 | # that the client is willing to accept a response whose age is no
109 | # greater than the specified time in seconds.
110 | def max_age
111 | self['max-age'].to_i if key?('max-age')
112 | end
113 |
114 | # If a response includes an s-maxage directive, then for a shared
115 | # cache (but not for a private cache), the maximum age specified by
116 | # this directive overrides the maximum age specified by either the
117 | # max-age directive or the expires header. The s-maxage directive
118 | # also implies the semantics of the proxy-revalidate directive. i.e.,
119 | # that the shared cache must not use the entry after it becomes stale
120 | # to respond to a subsequent request without first revalidating it with
121 | # the origin server. The s-maxage directive is always ignored by a
122 | # private cache.
123 | def shared_max_age
124 | self['s-maxage'].to_i if key?('s-maxage')
125 | end
126 | alias_method :s_maxage, :shared_max_age
127 |
128 | # If a response includes a r-maxage directive, then for a reverse cache
129 | # (but not for a private or proxy cache), the maximum age specified by
130 | # this directive overrides the maximum age specified by either the max-age
131 | # directive, the s-maxage directive, or the expires header. The r-maxage
132 | # directive also implies the semantics of the proxy-revalidate directive.
133 | # i.e., that the reverse cache must not use the entry after it becomes
134 | # stale to respond to a subsequent request without first revalidating it
135 | # with the origin server. The r-maxage directive is always ignored by
136 | # private and proxy caches.
137 | def reverse_max_age
138 | self['r-maxage'].to_i if key?('r-maxage')
139 | end
140 | alias_method :r_maxage, :reverse_max_age
141 |
142 | # Because a cache MAY be configured to ignore a server's specified
143 | # expiration time, and because a client request MAY include a max-
144 | # stale directive (which has a similar effect), the protocol also
145 | # includes a mechanism for the origin server to require revalidation
146 | # of a cache entry on any subsequent use. When the must-revalidate
147 | # directive is present in a response received by a cache, that cache
148 | # MUST NOT use the entry after it becomes stale to respond to a
149 | # subsequent request without first revalidating it with the origin
150 | # server. (I.e., the cache MUST do an end-to-end revalidation every
151 | # time, if, based solely on the origin server's expires or max-age
152 | # value, the cached response is stale.)
153 | #
154 | # The must-revalidate directive is necessary to support reliable
155 | # operation for certain protocol features. In all circumstances an
156 | # HTTP/1.1 cache MUST obey the must-revalidate directive; in
157 | # particular, if the cache cannot reach the origin server for any
158 | # reason, it MUST generate a 504 (Gateway Timeout) response.
159 | #
160 | # Servers SHOULD send the must-revalidate directive if and only if
161 | # failure to revalidate a request on the entity could result in
162 | # incorrect operation, such as a silently unexecuted financial
163 | # transaction. Recipients MUST NOT take any automated action that
164 | # violates this directive, and MUST NOT automatically provide an
165 | # unvalidated copy of the entity if revalidation fails.
166 | def must_revalidate?
167 | self['must-revalidate']
168 | end
169 |
170 | # The proxy-revalidate directive has the same meaning as the must-
171 | # revalidate directive, except that it does not apply to non-shared
172 | # user agent caches. It can be used on a response to an
173 | # authenticated request to permit the user's cache to store and
174 | # later return the response without needing to revalidate it (since
175 | # it has already been authenticated once by that user), while still
176 | # requiring proxies that service many users to revalidate each time
177 | # (in order to make sure that each user has been authenticated).
178 | # Note that such authenticated responses also need the public cache
179 | # control directive in order to allow them to be cached at all.
180 | def proxy_revalidate?
181 | self['proxy-revalidate']
182 | end
183 |
184 | def to_s
185 | bools, vals = [], []
186 | each do |key,value|
187 | if value == true
188 | bools << key
189 | elsif value
190 | vals << "#{key}=#{value}"
191 | end
192 | end
193 | (bools.sort + vals.sort).join(', ')
194 | end
195 |
196 | private
197 |
198 | def parse(value)
199 | return if value.nil? || value.empty?
200 | value.delete(' ').split(',').each do |part|
201 | next if part.empty?
202 | name, value = part.split('=', 2)
203 | self[name.downcase] = (value || true) unless name.empty?
204 | end
205 | self
206 | end
207 | end
208 | end
209 | end
210 |
--------------------------------------------------------------------------------
/lib/rack/cache/cachecontrol.rb:
--------------------------------------------------------------------------------
1 | warn "use require 'rack/cache/cache_control'"
2 | require 'rack/cache/cache_control'
3 |
--------------------------------------------------------------------------------
/lib/rack/cache/context.rb:
--------------------------------------------------------------------------------
1 | require 'rack/cache/options'
2 | require 'rack/cache/request'
3 | require 'rack/cache/response'
4 | require 'rack/cache/storage'
5 |
6 | module Rack::Cache
7 | # Implements Rack's middleware interface and provides the context for all
8 | # cache logic, including the core logic engine.
9 | class Context
10 | include Rack::Cache::Options
11 |
12 | # Array of trace Symbols
13 | attr_reader :trace
14 |
15 | # The Rack application object immediately downstream.
16 | attr_reader :backend
17 |
18 | def initialize(backend, options={})
19 | @backend = backend
20 | @trace = []
21 | @env = nil
22 | @options = options
23 |
24 | initialize_options options
25 | yield self if block_given?
26 |
27 | @private_header_keys =
28 | private_headers.map { |name| "HTTP_#{name.upcase.tr('-', '_')}" }
29 | end
30 |
31 | # The configured MetaStore instance. Changing the rack-cache.metastore
32 | # value effects the result of this method immediately.
33 | def metastore
34 | uri = options['rack-cache.metastore']
35 | storage.resolve_metastore_uri(uri, @options)
36 | end
37 |
38 | # The configured EntityStore instance. Changing the rack-cache.entitystore
39 | # value effects the result of this method immediately.
40 | def entitystore
41 | uri = options['rack-cache.entitystore']
42 | storage.resolve_entitystore_uri(uri, @options)
43 | end
44 |
45 | # The Rack call interface. The receiver acts as a prototype and runs
46 | # each request in a dup object unless the +rack.run_once+ variable is
47 | # set in the environment.
48 | def call(env)
49 | if env['rack.run_once'] && !env['rack.multithread']
50 | call! env
51 | else
52 | clone.call! env
53 | end
54 | end
55 |
56 | # The real Rack call interface. The caching logic is performed within
57 | # the context of the receiver.
58 | def call!(env)
59 | @trace = []
60 | @default_options.each { |k,v| env[k] ||= v }
61 | @env = env
62 | @request = Request.new(@env.dup.freeze)
63 |
64 | response =
65 | if @request.get? || @request.head?
66 | if !@env['HTTP_EXPECT'] && !@env['rack-cache.force-pass']
67 | lookup
68 | else
69 | pass
70 | end
71 | else
72 | if @request.options?
73 | pass
74 | else
75 | invalidate
76 | end
77 | end
78 |
79 | # log trace and set x-rack-cache tracing header
80 | trace = @trace.join(', ')
81 | response.headers['x-rack-cache'] = trace
82 |
83 | # write log message to rack.errors
84 | if verbose?
85 | message = "cache: [%s %s] %s\n" %
86 | [@request.request_method, @request.fullpath, trace]
87 | log_info(message)
88 | end
89 |
90 | # tidy up response a bit
91 | if (@request.get? || @request.head?) && not_modified?(response)
92 | response.not_modified!
93 | end
94 |
95 | if @request.head?
96 | response.body.close if response.body.respond_to?(:close)
97 | response.body = []
98 | end
99 | response.to_a
100 | end
101 |
102 | private
103 |
104 | # Record that an event took place.
105 | def record(event)
106 | @trace << event
107 | end
108 |
109 | # Does the request include authorization or other sensitive information
110 | # that should cause the response to be considered private by default?
111 | # Private responses are not stored in the cache.
112 | def private_request?
113 | @private_header_keys.any? { |key| @env.key?(key) }
114 | end
115 |
116 | # Determine if the #response validators (etag, last-modified) matches
117 | # a conditional value specified in #request.
118 | def not_modified?(response)
119 | last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
120 | if etags = @request.env['HTTP_IF_NONE_MATCH']
121 | etags = etags.split(/\s*,\s*/)
122 | (etags.include?(response.etag) || etags.include?('*')) && (!last_modified || response.last_modified == last_modified)
123 | elsif last_modified
124 | response.last_modified == last_modified
125 | end
126 | end
127 |
128 | # Whether the cache entry is "fresh enough" to satisfy the request.
129 | def fresh_enough?(entry)
130 | if entry.fresh?
131 | if allow_revalidate? && max_age = @request.cache_control.max_age
132 | max_age > 0 && max_age >= entry.age
133 | else
134 | true
135 | end
136 | end
137 | end
138 |
139 | # Delegate the request to the backend and create the response.
140 | def forward
141 | Response.new(*backend.call(@env))
142 | end
143 |
144 | # The request is sent to the backend, and the backend's response is sent
145 | # to the client, but is not entered into the cache.
146 | def pass
147 | record :pass
148 | forward
149 | end
150 |
151 | # Invalidate POST, PUT, DELETE and all methods not understood by this cache
152 | # See RFC2616 13.10
153 | def invalidate
154 | metastore.invalidate(@request, entitystore)
155 | rescue => e
156 | log_error(e)
157 | pass
158 | else
159 | record :invalidate
160 | pass
161 | end
162 |
163 | # Try to serve the response from cache. When a matching cache entry is
164 | # found and is fresh, use it as the response without forwarding any request
165 | # to the backend. When a matching cache entry is found but is stale, attempt
166 | # to #validate the entry with the backend using conditional GET.
167 | # If validation raises an exception and fault tolerant caching is enabled,
168 | # serve the stale cache entry.
169 | # When no matching cache entry is found, trigger miss processing.
170 | def lookup
171 | if @request.no_cache? && allow_reload?
172 | record :reload
173 | fetch
174 | else
175 | begin
176 | entry = metastore.lookup(@request, entitystore)
177 | rescue => e
178 | log_error(e)
179 | return pass
180 | end
181 | if entry
182 | if fresh_enough?(entry)
183 | record :fresh
184 | entry.headers['age'] = entry.age.to_s
185 | entry
186 | else
187 | record :stale
188 | if fault_tolerant?
189 | validate_with_stale_cache_failover(entry)
190 | else
191 | validate(entry)
192 | end
193 | end
194 | else
195 | record :miss
196 | fetch
197 | end
198 | end
199 | end
200 |
201 | # Returns stale cache on exception.
202 | def validate_with_stale_cache_failover(entry)
203 | validate(entry)
204 | rescue => e
205 | record :connnection_failed
206 | age = entry.age.to_s
207 | entry.headers['age'] = age
208 | record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e}"
209 | entry
210 | end
211 |
212 | # Validate that the cache entry is fresh. The original request is used
213 | # as a template for a conditional GET request with the backend.
214 | def validate(entry)
215 | # send no head requests because we want content
216 | convert_head_to_get!
217 |
218 | # add our cached last-modified validator to the environment
219 | @env['HTTP_IF_MODIFIED_SINCE'] = entry.last_modified
220 |
221 | # Add our cached etag validator to the environment.
222 | # We keep the etags from the client to handle the case when the client
223 | # has a different private valid entry which is not cached here.
224 | cached_etags = entry.etag.to_s.split(/\s*,\s*/)
225 | request_etags = @request.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
226 | etags = (cached_etags + request_etags).uniq
227 | @env['HTTP_IF_NONE_MATCH'] = etags.empty? ? nil : etags.join(', ')
228 |
229 | response = forward
230 |
231 | if response.status == 304
232 | record :valid
233 |
234 | # Check if the response validated which is not cached here
235 | etag = response.headers['etag']
236 | return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)
237 |
238 | entry = entry.dup
239 | entry.headers.delete('date')
240 | %w[Date expires cache-control etag last-modified].each do |name|
241 | next unless value = response.headers[name]
242 | entry.headers[name] = value
243 | end
244 |
245 | # even though it's empty, be sure to close the response body from upstream
246 | # because middleware use close to signal end of response
247 | response.body.close if response.body.respond_to?(:close)
248 |
249 | response = entry
250 | else
251 | record :invalid
252 | end
253 |
254 | store(response) if response.cacheable?
255 |
256 | response
257 | end
258 |
259 | # The cache missed or a reload is required. Forward the request to the
260 | # backend and determine whether the response should be stored. This allows
261 | # conditional / validation requests through to the backend but performs no
262 | # caching of the response when the backend returns a 304.
263 | def fetch
264 | # send no head requests because we want content
265 | convert_head_to_get!
266 |
267 | response = forward
268 |
269 | # Mark the response as explicitly private if any of the private
270 | # request headers are present and the response was not explicitly
271 | # declared public.
272 | if private_request? && !response.cache_control.public?
273 | response.private = true
274 | elsif default_ttl > 0 && response.ttl.nil? && !response.cache_control.must_revalidate?
275 | # assign a default TTL for the cache entry if none was specified in
276 | # the response; the must-revalidate cache control directive disables
277 | # default ttl assigment.
278 | response.ttl = default_ttl
279 | end
280 |
281 | store(response) if response.cacheable?
282 |
283 | response
284 | end
285 |
286 | # Write the response to the cache.
287 | def store(response)
288 | strip_ignore_headers(response)
289 | metastore.store(@request, response, entitystore)
290 | response.headers['age'] = response.age.to_s
291 | rescue => e
292 | log_error(e)
293 | nil
294 | else
295 | record :store
296 | end
297 |
298 | # Remove all ignored response headers before writing to the cache.
299 | def strip_ignore_headers(response)
300 | stripped_values = ignore_headers.map { |name| response.headers.delete(name) }
301 | record :ignore if stripped_values.any?
302 | end
303 |
304 | def log_error(exception)
305 | message = "cache error: #{exception.message}\n#{exception.backtrace.join("\n")}\n"
306 | log(:error, message)
307 | end
308 |
309 | def log_info(message)
310 | log(:info, message)
311 | end
312 |
313 | def log(level, message)
314 | if @env['rack.logger']
315 | @env['rack.logger'].send(level, message)
316 | else
317 | @env['rack.errors'].write(message)
318 | end
319 | end
320 |
321 | # send no head requests because we want content
322 | def convert_head_to_get!
323 | if @env['REQUEST_METHOD'] == 'HEAD'
324 | @env['REQUEST_METHOD'] = 'GET'
325 | @env['rack.methodoverride.original_method'] = 'HEAD'
326 | end
327 | end
328 | end
329 | end
330 |
--------------------------------------------------------------------------------
/lib/rack/cache/entity_store.rb:
--------------------------------------------------------------------------------
1 | require 'digest/sha1'
2 |
3 | module Rack::Cache
4 |
5 | # Entity stores are used to cache response bodies across requests. All
6 | # Implementations are required to calculate a SHA checksum of the data written
7 | # which becomes the response body's key.
8 | class EntityStore
9 |
10 | # Read body calculating the SHA1 checksum and size while
11 | # yielding each chunk to the block. If the body responds to close,
12 | # call it after iteration is complete. Return a two-tuple of the form:
13 | # [ hexdigest, size ].
14 | def slurp(body)
15 | digest, size = Digest::SHA1.new, 0
16 | body.each do |part|
17 | size += bytesize(part)
18 | digest << part
19 | yield part
20 | end
21 | body.close if body.respond_to? :close
22 | [digest.hexdigest, size]
23 | end
24 |
25 | if ''.respond_to?(:bytesize)
26 | def bytesize(string); string.bytesize; end
27 | else
28 | def bytesize(string); string.size; end
29 | end
30 |
31 | private :slurp, :bytesize
32 |
33 |
34 | # Stores entity bodies on the heap using a Hash object.
35 | class Heap < EntityStore
36 | # Create the store with the specified backing Hash.
37 | def initialize(hash={}, options = {})
38 | @hash = hash
39 | @options = options
40 | end
41 |
42 | # Determine whether the response body with the specified key (SHA1)
43 | # exists in the store.
44 | def exist?(key)
45 | @hash.include?(key)
46 | end
47 |
48 | # Return an object suitable for use as a Rack response body for the
49 | # specified key.
50 | def open(key)
51 | (body = @hash[key]) && body.dup
52 | end
53 |
54 | # Read all data associated with the given key and return as a single
55 | # String.
56 | def read(key)
57 | (body = @hash[key]) && body.join
58 | end
59 |
60 | # Write the Rack response body immediately and return the SHA1 key.
61 | def write(body, ttl=nil)
62 | buf = []
63 | key, size = slurp(body) { |part| buf << part }
64 | @hash[key] = buf
65 | [key, size]
66 | end
67 |
68 | # Remove the body corresponding to key; return nil.
69 | def purge(key)
70 | @hash.delete(key)
71 | nil
72 | end
73 |
74 | def self.resolve(uri, options = {})
75 | new({}, options)
76 | end
77 | end
78 |
79 | HEAP = Heap
80 | MEM = Heap
81 |
82 | # Stores entity bodies on disk at the specified path.
83 | class Disk < EntityStore
84 |
85 | # Path where entities should be stored. This directory is
86 | # created the first time the store is instansiated if it does not
87 | # already exist.
88 | attr_reader :root
89 |
90 | def initialize(root)
91 | @root = root
92 | FileUtils.mkdir_p root, :mode => 0755
93 | end
94 |
95 | def exist?(key)
96 | File.exist?(body_path(key))
97 | end
98 |
99 | def read(key)
100 | File.open(body_path(key), 'rb') { |f| f.read }
101 | rescue Errno::ENOENT
102 | nil
103 | end
104 |
105 | class Body < ::File #:nodoc:
106 | def each
107 | while part = read(8192)
108 | yield part
109 | end
110 | end
111 | alias_method :to_path, :path
112 | end
113 |
114 | # Open the entity body and return an IO object. The IO object's
115 | # each method is overridden to read 8K chunks instead of lines.
116 | def open(key)
117 | Body.open(body_path(key), 'rb')
118 | rescue Errno::ENOENT
119 | nil
120 | end
121 |
122 | def write(body, ttl=nil)
123 | filename = ['buf', $$, Thread.current.object_id].join('-')
124 | temp_file = storage_path(filename)
125 | key, size =
126 | File.open(temp_file, 'wb') { |dest|
127 | slurp(body) { |part| dest.write(part) }
128 | }
129 |
130 | path = body_path(key)
131 | if File.exist?(path)
132 | File.unlink temp_file
133 | else
134 | FileUtils.mkdir_p File.dirname(path), :mode => 0755
135 | FileUtils.mv temp_file, path
136 | end
137 | [key, size]
138 | end
139 |
140 | def purge(key)
141 | File.unlink body_path(key)
142 | nil
143 | rescue Errno::ENOENT
144 | nil
145 | end
146 |
147 | protected
148 | def storage_path(stem)
149 | File.join root, stem
150 | end
151 |
152 | def spread(key)
153 | key = key.dup
154 | key[2,0] = '/'
155 | key
156 | end
157 |
158 | def body_path(key)
159 | storage_path spread(key)
160 | end
161 |
162 | def self.resolve(uri)
163 | path = File.expand_path(uri.opaque || uri.path)
164 | new path
165 | end
166 | end
167 |
168 | DISK = Disk
169 | FILE = Disk
170 |
171 | # Base class for memcached entity stores.
172 | class MemCacheBase < EntityStore
173 | # The underlying Memcached instance used to communicate with the
174 | # memcached daemon.
175 | attr_reader :cache
176 |
177 | extend Rack::Utils
178 |
179 | def open(key)
180 | data = read(key)
181 | data && [data]
182 | end
183 |
184 | def self.resolve(uri)
185 | if uri.respond_to?(:scheme)
186 | server = "#{uri.host}:#{uri.port || '11211'}"
187 | options = parse_query(uri.query)
188 | options.keys.each do |key|
189 | value =
190 | case value = options.delete(key)
191 | when 'true' ; true
192 | when 'false' ; false
193 | else value.to_sym
194 | end
195 | options[key.to_sym] = value
196 | end
197 | options[:namespace] = uri.path.sub(/^\//, '')
198 | new server, options
199 | else
200 | # if the object provided is not a URI, pass it straight through
201 | # to the underlying implementation.
202 | new uri
203 | end
204 | end
205 | end
206 |
207 | # Uses the Dalli ruby library. This is the default unless
208 | # the memcached library has already been required.
209 | class Dalli < MemCacheBase
210 | def initialize(server="localhost:11211", options={})
211 | @cache =
212 | if server.respond_to?(:stats)
213 | server
214 | else
215 | require 'dalli'
216 | ::Dalli::Client.new(server, options)
217 | end
218 | end
219 |
220 | def exist?(key)
221 | !cache.get(key).nil?
222 | end
223 |
224 | def read(key)
225 | data = cache.get(key)
226 | data.force_encoding('BINARY') if data.respond_to?(:force_encoding)
227 | data
228 | end
229 |
230 | def write(body, ttl=nil)
231 | buf = StringIO.new
232 | key, size = slurp(body){|part| buf.write(part) }
233 | [key, size] if cache.set(key, buf.string, ttl)
234 | end
235 |
236 | def purge(key)
237 | cache.delete(key)
238 | nil
239 | end
240 | end
241 |
242 | # Uses the memcached client library. The ruby based memcache-client is used
243 | # in preference to this store unless the memcached library has already been
244 | # required.
245 | class MemCached < MemCacheBase
246 | def initialize(server="localhost:11211", options={})
247 | options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
248 | @cache =
249 | if server.respond_to?(:stats)
250 | server
251 | else
252 | require 'memcached'
253 | ::Memcached.new(server, options)
254 | end
255 | end
256 |
257 | def exist?(key)
258 | cache.append(key, '')
259 | true
260 | rescue ::Memcached::NotStored
261 | false
262 | end
263 |
264 | def read(key)
265 | cache.get(key, false)
266 | rescue ::Memcached::NotFound
267 | nil
268 | end
269 |
270 | def write(body, ttl=0)
271 | buf = StringIO.new
272 | key, size = slurp(body){|part| buf.write(part) }
273 | cache.set(key, buf.string, ttl, false)
274 | [key, size]
275 | end
276 |
277 | def purge(key)
278 | cache.delete(key)
279 | nil
280 | rescue ::Memcached::NotFound
281 | nil
282 | end
283 | end
284 |
285 | MEMCACHE = Dalli
286 | MEMCACHED = MEMCACHE
287 |
288 | class GAEStore < EntityStore
289 | attr_reader :cache
290 |
291 | def initialize(options = {})
292 | require 'rack/cache/app_engine'
293 | @cache = Rack::Cache::AppEngine::MemCache.new(options)
294 | end
295 |
296 | def exist?(key)
297 | cache.contains?(key)
298 | end
299 |
300 | def read(key)
301 | cache.get(key)
302 | end
303 |
304 | def open(key)
305 | if data = read(key)
306 | [data]
307 | else
308 | nil
309 | end
310 | end
311 |
312 | def write(body, ttl=nil)
313 | buf = StringIO.new
314 | key, size = slurp(body){|part| buf.write(part) }
315 | cache.put(key, buf.string, ttl)
316 | [key, size]
317 | end
318 |
319 | def purge(key)
320 | cache.delete(key)
321 | nil
322 | end
323 |
324 | def self.resolve(uri)
325 | self.new(:namespace => uri.host)
326 | end
327 |
328 | end
329 |
330 | GAECACHE = GAEStore
331 | GAE = GAEStore
332 |
333 | # Noop Entity Store backend.
334 | #
335 | # Set `entitystore` to 'noop:/'.
336 | # Does not persist response bodies (no disk/memory used).
337 | # Responses from the cache will have an empty body.
338 | # Clients must ignore these empty cached response (check for x-rack-cache response header).
339 | # Atm cannot handle streamed responses, patch needed.
340 | #
341 | class Noop < EntityStore
342 | def exist?(key)
343 | true
344 | end
345 |
346 | def read(key)
347 | ''
348 | end
349 |
350 | def open(key)
351 | []
352 | end
353 |
354 | def write(body, ttl=nil)
355 | key, size = slurp(body) { |part| part }
356 | [key, size]
357 | end
358 |
359 | def purge(key)
360 | nil
361 | end
362 |
363 | def self.resolve(uri)
364 | new
365 | end
366 | end
367 |
368 | NOOP = Noop
369 | end
370 |
371 | end
372 |
--------------------------------------------------------------------------------
/lib/rack/cache/entitystore.rb:
--------------------------------------------------------------------------------
1 | warn "use require 'rack/cache/entity_store'"
2 | require 'rack/cache/entity_store'
3 |
--------------------------------------------------------------------------------
/lib/rack/cache/headers.rb:
--------------------------------------------------------------------------------
1 | module Rack::Cache
2 | begin
3 | # For `Rack::Headers` (Rack 3+):
4 | require "rack/headers"
5 | Headers = ::Rack::Headers
6 | def self.Headers(headers)
7 | Headers[headers]
8 | end
9 | rescue LoadError
10 | # For `Rack::Utils::HeaderHash`:
11 | require "rack/utils"
12 | Headers = ::Rack::Utils::HeaderHash
13 | def self.Headers(headers)
14 | if headers.is_a?(Headers) && !headers.frozen?
15 | return headers
16 | else
17 | return Headers.new(headers)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rack/cache/key.rb:
--------------------------------------------------------------------------------
1 | require 'rack/utils'
2 |
3 | module Rack::Cache
4 | class Key
5 | include Rack::Utils
6 |
7 | # A proc for ignoring parts of query strings when generating a key. This is
8 | # useful when you have parameters like `utm` or `trk` that don't affect the
9 | # content on the page and are unique per-visitor or campaign. Parameters
10 | # like these will be part of the key and cause a lot of churn.
11 | #
12 | # The block will be passed a key and value which are the name and value of
13 | # that parameter.
14 | #
15 | # Example:
16 | # `Rack::Cache::Key.query_string_ignore = proc { |k, v| k =~ /^(trk|utm)_/ }`
17 | #
18 | class << self
19 | attr_accessor :query_string_ignore
20 | end
21 |
22 | # Implement .call, since it seems like the "Rack-y" thing to do. Plus, it
23 | # opens the door for cache key generators to just be blocks.
24 | def self.call(request)
25 | new(request).generate
26 | end
27 |
28 | def initialize(request)
29 | @request = request
30 | end
31 |
32 | # Generate a normalized cache key for the request.
33 | def generate
34 | parts = []
35 | parts << @request.scheme << "://"
36 | parts << @request.host
37 |
38 | if @request.scheme == "https" && @request.port != 443 ||
39 | @request.scheme == "http" && @request.port != 80
40 | parts << ":" << @request.port.to_s
41 | end
42 |
43 | parts << @request.script_name
44 | parts << @request.path_info
45 |
46 | if qs = query_string
47 | parts << "?"
48 | parts << qs
49 | end
50 |
51 | parts.join
52 | end
53 |
54 | private
55 | # Build a normalized query string by alphabetizing all keys/values
56 | # and applying consistent escaping.
57 | def query_string
58 | return nil if @request.query_string.to_s.empty?
59 |
60 | parts = @request.query_string.split(/[&;] */n)
61 | parts.map! { |p| p.split('=', 2).map!{ |s| unescape(s) } }
62 | parts.sort!
63 | parts.reject!(&self.class.query_string_ignore)
64 | parts.map! { |k,v| "#{escape(k)}=#{escape(v)}" }
65 | parts.empty? ? nil : parts.join('&')
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/rack/cache/meta_store.rb:
--------------------------------------------------------------------------------
1 | require 'fileutils'
2 | require 'digest/sha1'
3 | require 'rack/utils'
4 | require 'rack/cache/key'
5 |
6 | module Rack::Cache
7 |
8 | # The MetaStore is responsible for storing meta information about a
9 | # request/response pair keyed by the request's URL.
10 | #
11 | # The meta store keeps a list of request/response pairs for each canonical
12 | # request URL. A request/response pair is a two element Array of the form:
13 | # [request, response]
14 | #
15 | # The +request+ element is a Hash of Rack environment keys. Only protocol
16 | # keys (i.e., those that start with "HTTP_") are stored. The +response+
17 | # element is a Hash of cached HTTP response headers for the paired request.
18 | #
19 | # The MetaStore class is abstract and should not be instanstiated
20 | # directly. Concrete subclasses should implement the protected #read,
21 | # #write, and #purge methods. Care has been taken to keep these low-level
22 | # methods dumb and straight-forward to implement.
23 | class MetaStore
24 |
25 | # Locate a cached response for the request provided. Returns a
26 | # Rack::Cache::Response object if the cache hits or nil if no cache entry
27 | # was found.
28 | def lookup(request, entity_store)
29 | key = cache_key(request)
30 | entries = read(key)
31 |
32 | # bail out if we have nothing cached
33 | return nil if entries.empty?
34 |
35 | # find a cached entry that matches the request.
36 | env = request.env
37 | match = entries.detect{ |req,res| requests_match?((res['vary'] || res['vary']), env, req) }
38 | return nil if match.nil?
39 |
40 | _, res = match
41 | entity_key = res['x-content-digest']
42 | if entity_key && body = entity_store.open(entity_key)
43 | restore_response(res, body)
44 | else
45 | # the metastore referenced an entity that doesn't exist in
46 | # the entitystore, purge the entry from the meta-store
47 | begin
48 | purge(key)
49 | rescue NotImplementedError
50 | @@warned_on_purge ||= begin
51 | warn "WARNING: Future releases may require purge implementation for #{self.class.name}"
52 | true
53 | end
54 | nil
55 | end
56 | end
57 | end
58 |
59 | # Write a cache entry to the store under the given key. Existing
60 | # entries are read and any that match the response are removed.
61 | # This method calls #write with the new list of cache entries.
62 | def store(request, response, entity_store)
63 | key = cache_key(request)
64 | stored_env = persist_request(request)
65 |
66 | # write the response body to the entity store if this is the
67 | # original response.
68 | if response.headers['x-content-digest'].nil?
69 | if request.env['rack-cache.use_native_ttl'] && response.fresh?
70 | digest, size = entity_store.write(response.body, response.ttl)
71 | else
72 | digest, size = entity_store.write(response.body)
73 | end
74 | response.headers['x-content-digest'] = digest
75 | response.headers['content-length'] = size.to_s unless response.headers['Transfer-Encoding']
76 |
77 | # If the entitystore backend is a Noop, do not try to read the body from the backend, it always returns an empty array
78 | unless entity_store.is_a? Rack::Cache::EntityStore::Noop
79 | # A stream body can only be read once and is currently closed by #write.
80 | # (To avoid having to keep giant objects in memory when writing to disk cache
81 | # the body is never converted to a single string)
82 | # We cannot always reply on body to be re-readable,
83 | # so we have to read it from the cache.
84 | # BUG: if the cache was unable to store a stream, the stream will be closed
85 | # and rack will try to read it again, resulting in hard to track down exception
86 | response.body = entity_store.open(digest) || response.body
87 | end
88 | end
89 |
90 | # read existing cache entries, remove non-varying, and add this one to
91 | # the list
92 | vary = response.vary
93 | entries =
94 | read(key).reject do |env, res|
95 | (vary == (res['vary'])) &&
96 | requests_match?(vary, env, stored_env)
97 | end
98 |
99 | headers = persist_response(response)
100 | headers.delete('age')
101 |
102 | entries.unshift [stored_env, headers]
103 | if request.env['rack-cache.use_native_ttl'] && response.fresh?
104 | write key, entries, response.ttl
105 | else
106 | write key, entries
107 | end
108 | key
109 | end
110 |
111 | # Generate a cache key for the request.
112 | def cache_key(request)
113 | keygen = request.env['rack-cache.cache_key'] || Key
114 | keygen.call(request)
115 | end
116 |
117 | # Invalidate all cache entries that match the request.
118 | def invalidate(request, entity_store)
119 | modified = false
120 | key = cache_key(request)
121 | entries =
122 | read(key).map do |req, res|
123 | response = restore_response(res)
124 | if response.fresh?
125 | response.expire!
126 | modified = true
127 | end
128 | [req, persist_response(response)]
129 | end
130 | write key, entries if modified
131 | end
132 |
133 | private
134 |
135 | # Extract the environment Hash from +request+ while making any
136 | # necessary modifications in preparation for persistence. The Hash
137 | # returned must be marshalable.
138 | def persist_request(request)
139 | env = request.env.dup
140 | env.reject! { |key,val| key =~ /[^0-9A-Z_]/ || !val.respond_to?(:to_str) }
141 | env
142 | end
143 |
144 | # Converts a stored response hash into a Response object. The caller
145 | # is responsible for loading and passing the body if needed.
146 | def restore_response(hash, body=[])
147 | status = hash.delete('x-status').to_i
148 | Rack::Cache::Response.new(status, hash, body)
149 | end
150 |
151 | def persist_response(response)
152 | hash = response.headers.dup
153 | hash['x-status'] = response.status.to_s
154 | hash
155 | end
156 |
157 | # Determine whether the two environment hashes are non-varying based on
158 | # the vary response header value provided.
159 | def requests_match?(vary, env1, env2)
160 | return true if vary.nil? || vary == ''
161 | vary.split(/[\s,]+/).all? do |header|
162 | key = "HTTP_#{header.upcase.tr('-', '_')}"
163 | env1[key] == env2[key]
164 | end
165 | end
166 |
167 | protected
168 | # Locate all cached request/response pairs that match the specified
169 | # URL key. The result must be an Array of all cached request/response
170 | # pairs. An empty Array must be returned if nothing is cached for
171 | # the specified key.
172 | def read(key)
173 | raise NotImplementedError
174 | end
175 |
176 | # Store an Array of request/response pairs for the given key. Concrete
177 | # implementations should not attempt to filter or concatenate the
178 | # list in any way.
179 | def write(key, negotiations, ttl = nil)
180 | raise NotImplementedError
181 | end
182 |
183 | # Remove all cached entries at the key specified. No error is raised
184 | # when the key does not exist.
185 | def purge(key)
186 | raise NotImplementedError
187 | end
188 |
189 | private
190 | # Generate a SHA1 hex digest for the specified string. This is a
191 | # simple utility method for meta store implementations.
192 | def hexdigest(data)
193 | Digest::SHA1.hexdigest(data)
194 | end
195 |
196 | public
197 | # Concrete MetaStore implementation that uses a simple Hash to store
198 | # request/response pairs on the heap.
199 | class Heap < MetaStore
200 | def initialize(hash={}, options = {})
201 | @hash = hash
202 | @options = options
203 | end
204 |
205 | def read(key)
206 | if data = @hash[key]
207 | Marshal.load(data)
208 | else
209 | []
210 | end
211 | end
212 |
213 | def write(key, entries, ttl = nil)
214 | @hash[key] = Marshal.dump(entries)
215 | end
216 |
217 | def purge(key)
218 | @hash.delete(key)
219 | nil
220 | end
221 |
222 | def to_hash
223 | @hash
224 | end
225 |
226 | def self.resolve(uri, options = {})
227 | new({}, options)
228 | end
229 | end
230 |
231 | HEAP = Heap
232 | MEM = HEAP
233 |
234 | # Concrete MetaStore implementation that stores request/response
235 | # pairs on disk.
236 | class Disk < MetaStore
237 | attr_reader :root
238 |
239 | def initialize(root="/tmp/rack-cache/meta-#{ARGV[0]}")
240 | @root = File.expand_path(root)
241 | FileUtils.mkdir_p(root, :mode => 0755)
242 | end
243 |
244 | def read(key)
245 | path = key_path(key)
246 | File.open(path, 'rb') { |io| Marshal.load(io) }
247 | rescue Errno::ENOENT, IOError
248 | []
249 | end
250 |
251 | def write(key, entries, ttl = nil)
252 | tries = 0
253 | begin
254 | path = key_path(key)
255 | File.open(path, 'wb') { |io| Marshal.dump(entries, io, -1) }
256 | rescue Errno::ENOENT, IOError
257 | Dir.mkdir(File.dirname(path), 0755)
258 | retry if (tries += 1) == 1
259 | end
260 | end
261 |
262 | def purge(key)
263 | path = key_path(key)
264 | File.unlink(path)
265 | nil
266 | rescue Errno::ENOENT, IOError
267 | nil
268 | end
269 |
270 | private
271 | def key_path(key)
272 | File.join(root, spread(hexdigest(key)))
273 | end
274 |
275 | def spread(sha, n=2)
276 | sha = sha.dup
277 | sha[n,0] = '/'
278 | sha
279 | end
280 |
281 | public
282 | def self.resolve(uri)
283 | path = File.expand_path(uri.opaque || uri.path)
284 | new path
285 | end
286 |
287 | end
288 |
289 | DISK = Disk
290 | FILE = Disk
291 |
292 | # Stores request/response pairs in memcached. Keys are not stored
293 | # directly since memcached has a 250-byte limit on key names. Instead,
294 | # the SHA1 hexdigest of the key is used.
295 | class MemCacheBase < MetaStore
296 | extend Rack::Utils
297 |
298 | # The MemCache object used to communicated with the memcached
299 | # daemon.
300 | attr_reader :cache
301 |
302 | # Create MemCache store for the given URI. The URI must specify
303 | # a host and may specify a port, namespace, and options:
304 | #
305 | # memcached://example.com:11211/namespace?opt1=val1&opt2=val2
306 | #
307 | # Query parameter names and values are documented with the memcached
308 | # library: http://tinyurl.com/4upqnd
309 | def self.resolve(uri)
310 | if uri.respond_to?(:scheme)
311 | server = "#{uri.host}:#{uri.port || '11211'}"
312 | options = parse_query(uri.query)
313 | options.keys.each do |key|
314 | value =
315 | case value = options.delete(key)
316 | when 'true' ; true
317 | when 'false' ; false
318 | else value.to_sym
319 | end
320 | options[key.to_sym] = value
321 | end
322 |
323 | options[:namespace] = uri.path.to_s.sub(/^\//, '')
324 |
325 | new server, options
326 | else
327 | # if the object provided is not a URI, pass it straight through
328 | # to the underlying implementation.
329 | new uri
330 | end
331 | end
332 | end
333 |
334 | class Dalli < MemCacheBase
335 | def initialize(server="localhost:11211", options={})
336 | @cache =
337 | if server.respond_to?(:stats)
338 | server
339 | else
340 | require 'dalli'
341 | ::Dalli::Client.new(server, options)
342 | end
343 | end
344 |
345 | def read(key)
346 | key = hexdigest(key)
347 | cache.get(key) || []
348 | end
349 |
350 | # Default TTL to zero, interpreted as "don't expire" by Memcached.
351 | def write(key, entries, ttl = 0)
352 | key = hexdigest(key)
353 | cache.set(key, entries, ttl)
354 | end
355 |
356 | def purge(key)
357 | cache.delete(hexdigest(key))
358 | nil
359 | end
360 | end
361 |
362 | class MemCached < MemCacheBase
363 | # The Memcached instance used to communicated with the memcached
364 | # daemon.
365 | attr_reader :cache
366 |
367 | def initialize(server="localhost:11211", options={})
368 | options[:prefix_key] ||= options.delete(:namespace) if options.key?(:namespace)
369 | @cache =
370 | if server.respond_to?(:stats)
371 | server
372 | else
373 | require 'memcached'
374 | Memcached.new(server, options)
375 | end
376 | end
377 |
378 | def read(key)
379 | key = hexdigest(key)
380 | cache.get(key)
381 | rescue Memcached::NotFound
382 | []
383 | end
384 |
385 | # Default TTL to zero, interpreted as "don't expire" by Memcached.
386 | def write(key, entries, ttl = 0)
387 | key = hexdigest(key)
388 | cache.set(key, entries, ttl)
389 | end
390 |
391 | def purge(key)
392 | key = hexdigest(key)
393 | cache.delete(key)
394 | nil
395 | rescue Memcached::NotFound
396 | nil
397 | end
398 | end
399 |
400 | MEMCACHE =
401 | if defined?(::Memcached)
402 | MemCached
403 | else
404 | Dalli
405 | end
406 | MEMCACHED = MEMCACHE
407 |
408 | class GAEStore < MetaStore
409 | attr_reader :cache
410 |
411 | def initialize(options = {})
412 | require 'rack/cache/app_engine'
413 | @cache = Rack::Cache::AppEngine::MemCache.new(options)
414 | end
415 |
416 | def read(key)
417 | key = hexdigest(key)
418 | cache.get(key) || []
419 | end
420 |
421 | def write(key, entries)
422 | key = hexdigest(key)
423 | cache.put(key, entries)
424 | end
425 |
426 | def purge(key)
427 | key = hexdigest(key)
428 | cache.delete(key)
429 | nil
430 | end
431 |
432 | def self.resolve(uri)
433 | self.new(:namespace => uri.host)
434 | end
435 |
436 | end
437 |
438 | GAECACHE = GAEStore
439 | GAE = GAEStore
440 |
441 | end
442 |
443 | end
444 |
--------------------------------------------------------------------------------
/lib/rack/cache/metastore.rb:
--------------------------------------------------------------------------------
1 | warn "use require 'rack/cache/meta_store'"
2 | require 'rack/cache/meta_store'
3 |
--------------------------------------------------------------------------------
/lib/rack/cache/options.rb:
--------------------------------------------------------------------------------
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.