acorn_cache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +4 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/Rakefile +10 -0
- data/acorn_cache.gemspec +30 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/acorn_cache/app_exception.rb +9 -0
- data/lib/acorn_cache/cache_control_header.rb +48 -0
- data/lib/acorn_cache/cache_controller.rb +52 -0
- data/lib/acorn_cache/cache_maintenance.rb +27 -0
- data/lib/acorn_cache/cache_reader.rb +18 -0
- data/lib/acorn_cache/cache_writer.rb +13 -0
- data/lib/acorn_cache/cached_response.rb +134 -0
- data/lib/acorn_cache/config.rb +85 -0
- data/lib/acorn_cache/freshness_rules.rb +19 -0
- data/lib/acorn_cache/request.rb +58 -0
- data/lib/acorn_cache/server_response.rb +83 -0
- data/lib/acorn_cache/storage.rb +27 -0
- data/lib/acorn_cache/version.rb +3 -0
- data/lib/acorn_cache.rb +37 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cd4a59e38e279614a289b1a8e1e6c235389f615b
|
4
|
+
data.tar.gz: 1558ff76e457e53fc6530c180cd5381ec050cbb9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3014f53f2ec919dcbcae36b7c81c0ce7c686bf71499b681c6fd29d9ba76cc85ee63afce3bbff3fb8d1517964c269b959d090746cd2ce7d78f50b34e9cd317d46
|
7
|
+
data.tar.gz: 52bda2e31c8780828b486f6b22aebcf740d26a17a6f019e6daf9877fab93365e4115659e088cfc47429f3898528c935501ba4c24ad0ce9cc08b3edf1f4678067
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Vincent J. DeVendra
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
# AcornCache
|
2
|
+
|
3
|
+
AcornCache is a Ruby HTTP proxy caching library that is lightweight, configurable and can be easily integrated with any Rack-based web application. AcornCache allows you to improve page load times and lighten the load on your server by allowing you to implement an in-memory cache shared by every client requesting a resource on your server.
|
4
|
+
|
5
|
+
Features currently available include the following:
|
6
|
+
|
7
|
+
* Honors origin server cache control directives according to RFC2616 standards unless directed otherwise.
|
8
|
+
* Allows for easily configuring:
|
9
|
+
* which resources should be cached,
|
10
|
+
* for how long, and
|
11
|
+
* whether query params should be ignored
|
12
|
+
* Allows for basic browser caching behavior modification by changing out cache control header directives.
|
13
|
+
* Uses Redis or Memcached to store cached server responses.
|
14
|
+
* Adds a custom header to mark responses returned from the cache (`X-Acorn-Cache: HIT`)
|
15
|
+
|
16
|
+
##Getting Started
|
17
|
+
|
18
|
+
####Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'acorn_cache'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself:
|
31
|
+
|
32
|
+
$ gem install acorn_cache
|
33
|
+
|
34
|
+
|
35
|
+
AcornCache must be included in the middleware pipeline of your Rails or Rack application.
|
36
|
+
|
37
|
+
With Rails, add the following config option to the appropriate environment, probably ```config/environments/production.rb```. Note that we recommend AcornCache be positioned at the top of your middleware stack. Replace `Rack::Sendfile` in the example if necessary.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
config.middleware.insert_before(Rack::Sendfile, Rack::AcornCache)
|
41
|
+
```
|
42
|
+
|
43
|
+
You should now see ```Rack::AcornCache``` listed at the top of your middleware pipeline when you run `rake middleware`.
|
44
|
+
|
45
|
+
For non-Rails Rack apps, just include the following in your rackup (.ru) file:
|
46
|
+
```ruby
|
47
|
+
require 'acorn_cache'
|
48
|
+
|
49
|
+
use Rack::AcornCache
|
50
|
+
```
|
51
|
+
|
52
|
+
####Setting Up Storage
|
53
|
+
By default, AcornCache uses Redis to store server responses. You must include
|
54
|
+
your Redis host, port, and password (if you have one set) as environment variables.
|
55
|
+
|
56
|
+
```
|
57
|
+
ACORNCACHE_REDIS_HOST="your_host_name"
|
58
|
+
ACORNCACHE_REDIS_PORT="your_port_number"
|
59
|
+
ACORNCACHE_REDIS_PASSWORD="your_password"
|
60
|
+
```
|
61
|
+
You may also choose to use memcached. If so, set the URL (including host and
|
62
|
+
port) and, if you have SASL authentication, username and password.
|
63
|
+
|
64
|
+
```
|
65
|
+
ACORNCACHE_MEMCACHED_URL="your_url"
|
66
|
+
ACORNCACHE_MEMCACHED_USERNAME="your_username"
|
67
|
+
ACORNCACHE_MEMCACHED_PASSWORD="your_password"
|
68
|
+
```
|
69
|
+
To switch to Memcached, add the following line to your AcornCache config:
|
70
|
+
```ruby
|
71
|
+
config.storage = :memcached
|
72
|
+
```
|
73
|
+
|
74
|
+
####Configuration
|
75
|
+
AcornCache has a range of configuration options. If you're using Rails, set them in an initializer: `config/initializers/acorn_cache.rb`
|
76
|
+
|
77
|
+
Without configuration, AcornCache won't cache anything. Two basic configuration
|
78
|
+
patterns are possible. The most common will be to specify page rules telling
|
79
|
+
AcornCache how long to store a resource.
|
80
|
+
|
81
|
+
The config below specifies two URLs to cache and specifies the time to live, i.e., the time the resource at that location should live in AcornCache and the browser cache. With this config, AcornCache will only cache the resources at these two URLs:
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
if Rails.env.production?
|
87
|
+
Rack::AcornCache.configure do |config|
|
88
|
+
config.page_rules = {
|
89
|
+
"http://example.com/" => { browser_cache_ttl: 30 },
|
90
|
+
"http://foo.com/bar" => { acorn_cache_ttl: 100 },
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
|
97
|
+
If you choose to do so, you can have AcornCache act as an RFC compliant
|
98
|
+
shared proxy-cache for every resource on your server. For information concerning standard RFC caching rules,
|
99
|
+
please refer to the Further Information section below. To operate in this mode, just set:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
config.cache_everything = true
|
103
|
+
```
|
104
|
+
Keep in mind that you can override standard caching behavior even when in cache everything mode by specifying a page rule.
|
105
|
+
|
106
|
+
See below for all available options.
|
107
|
+
|
108
|
+
## Page Rules
|
109
|
+
Configuration options can be set for individual URLs via the
|
110
|
+
`page-rules` config option. The value of `page-rules` must be set to a hash. The hash must have a key that is either 1) a URL string, or 2) a pattern that matches the URL of the page(s) for which you are setting the rule, and a value that specifies the caching rule(s) for the page or pages. Here's an example:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
Rack::AcornCache.configure do |config|
|
114
|
+
config.page_rules = {
|
115
|
+
{ "http://foo.com" => { acorn_cache_ttl: 3600,
|
116
|
+
browser_cache_ttl: 800,
|
117
|
+
"http://bar.com/*" => { browser_cache_ttl: 3600,
|
118
|
+
ignore_query_params: true },
|
119
|
+
/^https+:\/\/.+\.com/ => { respect_default_header: true,
|
120
|
+
ignore_query_params: true }
|
121
|
+
}
|
122
|
+
end
|
123
|
+
```
|
124
|
+
####Deciding Which Resources Are Cached
|
125
|
+
Resources best suited for caching are public (not behind authentication) and don't change very often.
|
126
|
+
AcornCache provides you with three options for defining the URLs for the resources that you want to cache:
|
127
|
+
|
128
|
+
1. You can define a single URL explicitly:
|
129
|
+
```ruby
|
130
|
+
"http://www.foobar.com/baz" => { acorn_cache_ttl: 100 }
|
131
|
+
```
|
132
|
+
|
133
|
+
2. You can use wildcards to identify multiple pages for a which a given set of rules applies:
|
134
|
+
```ruby
|
135
|
+
"http://foo*.com" => { browser_cache_ttl: 86400 }
|
136
|
+
```
|
137
|
+
|
138
|
+
3. You can use regex pattern matching simply by using a `Regexp` object as the
|
139
|
+
key:
|
140
|
+
```ruby
|
141
|
+
/^https+:\/\/.+\.com/ => { acorn_cache_ttl: 100 }
|
142
|
+
```
|
143
|
+
|
144
|
+
|
145
|
+
####Deciding How Resources Are Cached
|
146
|
+
#####Override Existing Cache Control Headers
|
147
|
+
Suppose you don't know or want to change the cache control headers provided by your server. AcornCache gives you the ability to control how a resource is
|
148
|
+
cached by both AcornCache and the browser cache simply by specifying the
|
149
|
+
appropriate page rule settings.
|
150
|
+
|
151
|
+
AcornCache provides four options, which can be set either as defaults or within
|
152
|
+
individual page rules.
|
153
|
+
|
154
|
+
1. `acorn_cache_ttl` -
|
155
|
+
This option specifies the time a resource should live in AcornCache before
|
156
|
+
expiring. It works by overriding the `s-maxage` directive in the cache control
|
157
|
+
header with the specified value. Time should be given in seconds. It also removes any directives that would
|
158
|
+
prevent caching in a shared proxy cache, like `private` or `no-store`.
|
159
|
+
|
160
|
+
2. `browser_cache_ttl` -
|
161
|
+
This option specifies the time in seconds a resource should live in private
|
162
|
+
browser caches before expiring. It works by overriding the `max-age` directive
|
163
|
+
in the cache control header with the specified value. It also removes any
|
164
|
+
directives that would prevent caching in a private cache, like `no-store`.
|
165
|
+
|
166
|
+
3. `ignore_query_params` -
|
167
|
+
If the query params in a request shouldn't affect the response from your server,
|
168
|
+
you can set this option to `true` so that all requests for a URL, regardless of
|
169
|
+
the specified params, share the same cache entry. This means that if a resource
|
170
|
+
living at `http://foo.com` is cached with AcornCache, a request to
|
171
|
+
`http://foo.com/?bar=baz` will respond with that cached resource without creating another
|
172
|
+
cache entry.
|
173
|
+
|
174
|
+
4. `must_revalidate` -
|
175
|
+
When set to `true`, the content of the cache will be checked against the origin server using `ETag` or `Last-Modified` headers. With this configuration, AcornCache will not use a cache entry without first revalidating it with the origin server.
|
176
|
+
|
177
|
+
These four options can be set either as defaults or for individual page rules.
|
178
|
+
Default settings apply to any page that AcornCache is allowed to cache unless
|
179
|
+
they are overwritten by a page rule. For example, if your
|
180
|
+
config looks like this...
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
RackAcornCache.configure do |config|
|
184
|
+
config.default_acorn_cache_ttl = 30
|
185
|
+
config.page_rules = {
|
186
|
+
"http://foo.com" => { use_defaults: true }
|
187
|
+
"http://bar.com" => { acorn_cache_ttl: 100 }
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
...then the server response returned by a request to `foo.com` will be cached in AcornCache for 30 seconds, but the server response returned by a request to `bar.com` will be cached for 100 seconds.
|
192
|
+
|
193
|
+
#####Respect Existing Cache Control Headers
|
194
|
+
AcornCache provides you with the ability to respect the cache control headers that were provided from the client or origin server. This can be achieved by setting `respect_existing_headers: true` for a page or given set of pages. This option is useful when you don't want to cache everything but you also want to control caching behavior by ensuring that responses come from your server with the proper cache control headers. If you choose this option, you will likely want to ensure that your response has an `s-maxage` directive, as AcornCache operates as a shared cache.
|
195
|
+
|
196
|
+
## Further Information
|
197
|
+
|
198
|
+
AcornCache's rules and caching guidelines strictly follow RFC 2616 standards. [This flow chart](http://i.imgur.com/o63TJAa.jpg) details the logic and rules that AcornCache is built upon and defines its default behavior.
|
199
|
+
|
200
|
+
## Contributing
|
201
|
+
|
202
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/acorncache/acorn-cache.
|
203
|
+
|
204
|
+
|
205
|
+
## License
|
206
|
+
|
207
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/acorn_cache.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'acorn_cache/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "acorn_cache"
|
8
|
+
spec.version = AcornCache::VERSION
|
9
|
+
spec.authors = ["Vincent J. DeVendra", "Perry Carbone"]
|
10
|
+
spec.email = ["VinceDeVendra@gmail.com", "perrycarb@gmail.com"]
|
11
|
+
|
12
|
+
spec.description = "AcornCache is a Ruby HTTP proxy caching library that is lightweight, configurable and can be easily integrated with any Rack-based web application. AcornCache allows you to improve page load times and lighten the load on your server by allowing you to implement an in-memory cache shared by every client requesting a resource on your server."
|
13
|
+
spec.summary = "A HTTP proxy caching library for Rack apps"
|
14
|
+
spec.homepage = "https://github.com/acorncache/acorn-cache"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
spec.add_development_dependency "mocha"
|
27
|
+
spec.add_runtime_dependency "rack", "~> 1.6"
|
28
|
+
spec.add_runtime_dependency "redis"
|
29
|
+
spec.add_runtime_dependency "dalli"
|
30
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "acorn_cache"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
class Rack::AcornCache
|
4
|
+
class CacheControlHeader
|
5
|
+
attr_accessor :max_age, :s_maxage, :no_cache, :no_store,
|
6
|
+
:must_revalidate, :private, :max_fresh, :max_stale
|
7
|
+
|
8
|
+
def initialize(header_string = "")
|
9
|
+
return unless header_string && !header_string.empty?
|
10
|
+
set_directive_instance_variables!(header_string)
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :max_stale?, :max_stale
|
14
|
+
alias_method :no_cache?, :no_cache
|
15
|
+
alias_method :private?, :private
|
16
|
+
alias_method :no_store?, :no_store
|
17
|
+
alias_method :must_revalidate?, :must_revalidate
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
instance_variables.map do |ivar|
|
21
|
+
directive = ivar.to_s.sub("@", "").sub("_", "-")
|
22
|
+
value = instance_variable_get(ivar)
|
23
|
+
next directive if value == true
|
24
|
+
next unless value
|
25
|
+
"#{directive}=#{value}"
|
26
|
+
end.compact.sort.join(", ")
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_directive_instance_variables!(header_string)
|
32
|
+
header_string.gsub(/\s+/, "").split(",").each do |directive, result|
|
33
|
+
k, v = directive.split("=")
|
34
|
+
instance_variable_set(variable_symbol(k), directive_value(v))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def variable_symbol(directive)
|
39
|
+
"@#{directive.gsub("-", "_")}".to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def directive_value(value)
|
43
|
+
return value.to_i if value =~ /^[0-9]+$/
|
44
|
+
return true if value.nil?
|
45
|
+
value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Rack::AcornCache
|
2
|
+
class CacheController
|
3
|
+
def initialize(request, app)
|
4
|
+
@request = request
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def response
|
9
|
+
if request.no_cache?
|
10
|
+
server_response = get_response_from_server
|
11
|
+
else
|
12
|
+
cached_response = check_for_cached_response
|
13
|
+
|
14
|
+
if cached_response.must_be_revalidated?
|
15
|
+
request.update_conditional_headers!(cached_response)
|
16
|
+
server_response = get_response_from_server
|
17
|
+
elsif !cached_response.fresh_for_request?(request)
|
18
|
+
server_response = get_response_from_server
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
CacheMaintenance
|
23
|
+
.new(request.cache_key, server_response, cached_response)
|
24
|
+
.update_cache
|
25
|
+
.response
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :request, :app
|
31
|
+
|
32
|
+
def get_response_from_server
|
33
|
+
begin
|
34
|
+
status, headers, body = @app.call(request.env)
|
35
|
+
rescue => e
|
36
|
+
raise AppException.new(e)
|
37
|
+
end
|
38
|
+
|
39
|
+
server_response = ServerResponse.new(status, headers, body)
|
40
|
+
|
41
|
+
if request.page_rule?
|
42
|
+
server_response.update_with_page_rules!(request.page_rule)
|
43
|
+
else
|
44
|
+
server_response
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def check_for_cached_response
|
49
|
+
CacheReader.read(request.cache_key) || NullCachedResponse.new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Rack::AcornCache
|
2
|
+
class CacheMaintenance
|
3
|
+
attr_reader :response, :cache_key, :server_response, :cached_response
|
4
|
+
|
5
|
+
def initialize(cache_key, server_response, cached_response)
|
6
|
+
@cache_key = cache_key
|
7
|
+
@server_response = server_response
|
8
|
+
@cached_response = cached_response
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_cache
|
12
|
+
if !server_response
|
13
|
+
@response = cached_response.add_acorn_cache_header!
|
14
|
+
elsif !server_response.cacheable? && !server_response.status_304?
|
15
|
+
@response = server_response
|
16
|
+
elsif server_response.cacheable?
|
17
|
+
@response = server_response.cache!(cache_key)
|
18
|
+
elsif cached_response.matches?(server_response)
|
19
|
+
@response = cached_response.update_date_and_recache!(cache_key)
|
20
|
+
else
|
21
|
+
@response = server_response
|
22
|
+
end
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Rack::AcornCache
|
4
|
+
module CacheReader
|
5
|
+
def self.read(cache_key)
|
6
|
+
response = storage.get(cache_key)
|
7
|
+
return false unless response
|
8
|
+
response_hash = JSON.parse(response)
|
9
|
+
CachedResponse.new(response_hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.storage
|
15
|
+
Rack::AcornCache.configuration.storage
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class Rack::AcornCache
|
5
|
+
class CachedResponse
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@cache_control_header, :s_maxage, :max_age, :no_cache?, :must_revalidate?
|
8
|
+
|
9
|
+
attr_reader :body, :status, :headers, :date
|
10
|
+
DEFAULT_MAX_AGE = 3600
|
11
|
+
|
12
|
+
def initialize(args={})
|
13
|
+
@body = args["body"]
|
14
|
+
@status = args["status"]
|
15
|
+
@headers = args["headers"]
|
16
|
+
@cache_control_header = CacheControlHeader.new(headers["Cache-Control"])
|
17
|
+
end
|
18
|
+
|
19
|
+
def must_be_revalidated?
|
20
|
+
no_cache? || must_revalidate?
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_date!
|
24
|
+
headers["Date"] = Time.now.httpdate
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize
|
28
|
+
{ headers: headers, status: status, body: body }.to_json
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
[status, headers, [body]]
|
33
|
+
end
|
34
|
+
|
35
|
+
def etag_header
|
36
|
+
headers["ETag"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def last_modified_header
|
40
|
+
headers["Last-Modified"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_date_and_recache!(cache_key)
|
44
|
+
cached_response.update_date!
|
45
|
+
CacheWriter.write(cache_key, cached_response.serialize)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_acorn_cache_header!
|
50
|
+
unless headers["X-Acorn-Cache"]
|
51
|
+
headers["X-Acorn-Cache"] = "HIT"
|
52
|
+
end
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def matches?(server_response)
|
57
|
+
if etag_header
|
58
|
+
server_response.etag_header == etag_header
|
59
|
+
elsif last_modified_header
|
60
|
+
server_response.last_modified_header == last_modified_header
|
61
|
+
else
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def time_to_live
|
67
|
+
s_maxage || max_age || (expiration_header_time - date)
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :stale_time_specified?, :time_to_live
|
71
|
+
|
72
|
+
def fresh?
|
73
|
+
expiration_date > Time.now
|
74
|
+
end
|
75
|
+
|
76
|
+
def date
|
77
|
+
@date ||= Time.httpdate(date_header)
|
78
|
+
end
|
79
|
+
|
80
|
+
def expiration_date
|
81
|
+
if s_maxage
|
82
|
+
date + s_maxage
|
83
|
+
elsif max_age
|
84
|
+
date + max_age
|
85
|
+
elsif expiration_header
|
86
|
+
expiration
|
87
|
+
else
|
88
|
+
date + DEFAULT_MAX_AGE
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def time_until_expiration
|
93
|
+
Time.now - expiration
|
94
|
+
end
|
95
|
+
|
96
|
+
def fresh_for_request?(request)
|
97
|
+
FreshnessRules.cached_response_fresh_for_request?(self, request)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def expiration_header_time
|
103
|
+
Time.httpdate(expiration_header)
|
104
|
+
end
|
105
|
+
|
106
|
+
def expiration_header
|
107
|
+
@expiration_header ||= headers["Expiration"]
|
108
|
+
end
|
109
|
+
|
110
|
+
def expiration
|
111
|
+
@expiration ||= Time.httpdate(expiration_header)
|
112
|
+
end
|
113
|
+
|
114
|
+
def date_header
|
115
|
+
headers["Date"]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class NullCachedResponse
|
120
|
+
def must_be_revalidated?
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
def matches?(server_response)
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
def update!; end
|
129
|
+
|
130
|
+
def fresh_for_request?(request)
|
131
|
+
false
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Rack::AcornCache
|
2
|
+
class << self
|
3
|
+
attr_accessor :configuration
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configure
|
7
|
+
self.configuration ||= Configuration.new
|
8
|
+
yield(configuration)
|
9
|
+
end
|
10
|
+
|
11
|
+
class Configuration
|
12
|
+
attr_writer :storage
|
13
|
+
attr_reader :page_rules
|
14
|
+
attr_accessor :default_acorn_cache_ttl, :default_browser_cache_ttl,
|
15
|
+
:cache_everything, :default_ignore_query_params, :default_must_revalidate
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@cache_everything = false
|
19
|
+
@storage = :redis
|
20
|
+
end
|
21
|
+
|
22
|
+
def page_rules=(user_page_rules)
|
23
|
+
@page_rules = user_page_rules.each_with_object({}) do |(k, v), result|
|
24
|
+
result[k] = build_page_rule(v)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def page_rule_for_url(url)
|
29
|
+
if cache_everything
|
30
|
+
return default_page_rule unless page_rules
|
31
|
+
no_page_rule_found = proc { return default_page_rule }
|
32
|
+
else
|
33
|
+
return nil unless page_rules
|
34
|
+
no_page_rule_found = proc { return nil }
|
35
|
+
end
|
36
|
+
|
37
|
+
page_rules.find(no_page_rule_found) do |k, _|
|
38
|
+
page_rule_key_matches_url?(k, url)
|
39
|
+
end.last
|
40
|
+
end
|
41
|
+
|
42
|
+
def storage
|
43
|
+
if @storage == :redis
|
44
|
+
Rack::AcornCache::Storage.redis
|
45
|
+
elsif @storage == :memcached
|
46
|
+
Rack::AcornCache::Storage.memcached
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def default_page_rule
|
53
|
+
{ acorn_cache_ttl: default_acorn_cache_ttl,
|
54
|
+
browser_cache_ttl: default_browser_cache_ttl,
|
55
|
+
ignore_query_params: default_ignore_query_params,
|
56
|
+
must_revalidate: default_must_revalidate }
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_page_rule(options)
|
60
|
+
options[:ignore_query_params] = default_ignore_query_params
|
61
|
+
|
62
|
+
return options if options[:respect_existing_headers] || options[:must_revalidate]
|
63
|
+
{ acorn_cache_ttl: default_acorn_cache_ttl,
|
64
|
+
browser_cache_ttl: default_browser_cache_ttl }.merge(options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def page_rule_key_matches_url?(page_rule_key, url)
|
68
|
+
return url =~ page_rule_key if page_rule_key.is_a?(Regexp)
|
69
|
+
string = page_rule_key.gsub("*", ".*")
|
70
|
+
url =~ /^#{string}$/
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
#Example config setup:
|
75
|
+
# Rack::AcornCache.configure do |config|
|
76
|
+
# config.cache_everything = true
|
77
|
+
# config.default_acorn_cache_ttl = 3600
|
78
|
+
# config.page_rules = {
|
79
|
+
# "http://example.com/*.js" => { browser_cache_ttl: 30,
|
80
|
+
# regex: true },
|
81
|
+
# "another_url" => { acorn_cache_ttl: 100 },
|
82
|
+
# "foo.com" => { respect_existing_headers: true }
|
83
|
+
# }
|
84
|
+
# end
|
85
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Rack::AcornCache
|
2
|
+
module FreshnessRules
|
3
|
+
def self.cached_response_fresh_for_request?(cached_response, request)
|
4
|
+
return false unless cached_response
|
5
|
+
if cached_response.fresh?
|
6
|
+
if request.max_age_more_restrictive?(cached_response)
|
7
|
+
return cached_response.date + request.max_age >= Time.now
|
8
|
+
elsif request.max_fresh
|
9
|
+
return cached_response.expiration_date - request.max_fresh >= Time.now
|
10
|
+
end
|
11
|
+
true
|
12
|
+
else
|
13
|
+
return false unless request.max_stale?
|
14
|
+
return true if request.max_stale == true
|
15
|
+
cached_response.expiration_date + request.max_stale >= Time.now
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
class Rack::AcornCache
|
4
|
+
class Request < Rack::Request
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@cache_control_header, :no_cache?, :max_age, :max_fresh,
|
7
|
+
:max_stale, :max_stale?
|
8
|
+
|
9
|
+
def initialize(env)
|
10
|
+
super
|
11
|
+
@cache_control_header = CacheControlHeader.new(@env["HTTP_CACHE_CONTROL"])
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_conditional_headers!(cached_response)
|
15
|
+
if cached_response.etag_header
|
16
|
+
self.if_none_match = cached_response.etag_header
|
17
|
+
end
|
18
|
+
|
19
|
+
if cached_response.last_modified_header
|
20
|
+
self.if_modified_since = cached_response.last_modified_header
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def max_age_more_restrictive?(cached_response)
|
25
|
+
cached_response.stale_time_specified? &&
|
26
|
+
max_age && max_age < cached_response.time_to_live
|
27
|
+
end
|
28
|
+
|
29
|
+
def cacheable?
|
30
|
+
get? && (config.cache_everything || page_rule?)
|
31
|
+
end
|
32
|
+
|
33
|
+
def page_rule
|
34
|
+
config.page_rule_for_url(url) if config
|
35
|
+
end
|
36
|
+
|
37
|
+
def cache_key
|
38
|
+
return base_url + path if page_rule? && page_rule[:ignore_query_params]
|
39
|
+
url
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :page_rule?, :page_rule
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def config
|
47
|
+
Rack::AcornCache.configuration
|
48
|
+
end
|
49
|
+
|
50
|
+
def if_none_match=(etag)
|
51
|
+
env["HTTP_IF_NONE_MATCH"] = etag
|
52
|
+
end
|
53
|
+
|
54
|
+
def if_modified_since=(last_modified)
|
55
|
+
env["HTTP_IF_MODIFIED_SINCE"] = last_modified
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class Rack::AcornCache
|
5
|
+
class ServerResponse < Rack::Response
|
6
|
+
CACHEABLE_STATUS_CODES = [200, 203, 300, 301, 302, 404, 410]
|
7
|
+
attr_reader :status, :headers, :body, :cache_control_header
|
8
|
+
|
9
|
+
def initialize(status, headers, body)
|
10
|
+
@status = status
|
11
|
+
@headers = headers
|
12
|
+
@body = body
|
13
|
+
@cache_control_header = CacheControlHeader.new(headers["Cache-Control"])
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_date!
|
17
|
+
@headers["Date"] = Time.now.httpdate unless @headers["Date"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def cacheable?
|
21
|
+
[:private?, :no_store?].none? { |directive| send(directive) } &&
|
22
|
+
CACHEABLE_STATUS_CODES.include?(status)
|
23
|
+
end
|
24
|
+
|
25
|
+
def status_304?
|
26
|
+
status == 304
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialize
|
30
|
+
{ status: status, headers: headers, body: body_string }.to_json
|
31
|
+
end
|
32
|
+
|
33
|
+
def body_string
|
34
|
+
result = ""
|
35
|
+
body.each { |part| result << part }
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_a
|
40
|
+
[status, headers, body]
|
41
|
+
end
|
42
|
+
|
43
|
+
def cache!(cache_key)
|
44
|
+
update_date!
|
45
|
+
CacheWriter.write(cache_key, serialize)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_with_page_rules!(page_rule)
|
50
|
+
if page_rule[:must_revalidate]
|
51
|
+
self.no_cache = true
|
52
|
+
self.must_revalidate = true
|
53
|
+
self.private = nil
|
54
|
+
self.max_age = nil
|
55
|
+
self.s_maxage = nil
|
56
|
+
self.no_store = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
if page_rule[:acorn_cache_ttl] || page_rule[:browser_cache_ttl]
|
60
|
+
self.no_cache = nil
|
61
|
+
self.no_store = nil
|
62
|
+
self.must_revalidate = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
if page_rule[:acorn_cache_ttl]
|
66
|
+
self.max_age = nil
|
67
|
+
self.s_maxage = page_rule[:acorn_cache_ttl]
|
68
|
+
self.private = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
if page_rule[:browser_cache_ttl]
|
72
|
+
self.max_age = page_rule[:browser_cache_ttl]
|
73
|
+
end
|
74
|
+
|
75
|
+
headers["Cache-Control"] = cache_control_header.to_s
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_missing(method, *args)
|
80
|
+
cache_control_header.send(method, *args)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'dalli'
|
3
|
+
|
4
|
+
class Rack::AcornCache
|
5
|
+
module Storage
|
6
|
+
def self.redis
|
7
|
+
args = { host: ENV["ACORNCACHE_REDIS_HOST"],
|
8
|
+
port: ENV["ACORNCACHE_REDIS_PORT"].to_i }
|
9
|
+
if ENV["ACORNCACHE_REDIS_PASSWORD"]
|
10
|
+
args.merge!(password: ENV["ACORNCACHE_REDIS_PASSWORD"])
|
11
|
+
end
|
12
|
+
|
13
|
+
@redis ||= Redis.new(args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.memcached
|
17
|
+
options = {}
|
18
|
+
|
19
|
+
if ENV["ACORNCACHE_MEMCACHED_USERNAME"]
|
20
|
+
options = { username: ENV["ACORNCACHE_MEMCACHED_USERNAME"],
|
21
|
+
password: ENV["ACORNCACHE_MEMCACHED_PASSWORD"] }
|
22
|
+
end
|
23
|
+
|
24
|
+
@memcached ||= Dalli::Client.new(ENV["ACORNCACHE_MEMCACHED_URL"], options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/acorn_cache.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
class Rack::AcornCache
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
dup._call(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
def _call(env)
|
13
|
+
request = Request.new(env)
|
14
|
+
return @app.call(env) unless request.cacheable?
|
15
|
+
|
16
|
+
begin
|
17
|
+
CacheController.new(request, @app).response.to_a
|
18
|
+
rescue AppException => e
|
19
|
+
raise e.caught_exception
|
20
|
+
rescue => e
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'acorn_cache/request'
|
27
|
+
require 'acorn_cache/cache_controller'
|
28
|
+
require 'acorn_cache/app_exception'
|
29
|
+
require 'acorn_cache/config'
|
30
|
+
require 'acorn_cache/cache_control_header'
|
31
|
+
require 'acorn_cache/cache_writer'
|
32
|
+
require 'acorn_cache/freshness_rules'
|
33
|
+
require 'acorn_cache/storage'
|
34
|
+
require 'acorn_cache/cache_maintenance'
|
35
|
+
require 'acorn_cache/server_response'
|
36
|
+
require 'acorn_cache/cached_response'
|
37
|
+
require 'acorn_cache/cache_reader'
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acorn_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vincent J. DeVendra
|
8
|
+
- Perry Carbone
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-03-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.10'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.10'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: minitest
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '10.0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '10.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: pry
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: mocha
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rack
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '1.6'
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '1.6'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: redis
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
type: :runtime
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: dalli
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: AcornCache is a Ruby HTTP proxy caching library that is lightweight,
|
127
|
+
configurable and can be easily integrated with any Rack-based web application. AcornCache
|
128
|
+
allows you to improve page load times and lighten the load on your server by allowing
|
129
|
+
you to implement an in-memory cache shared by every client requesting a resource
|
130
|
+
on your server.
|
131
|
+
email:
|
132
|
+
- VinceDeVendra@gmail.com
|
133
|
+
- perrycarb@gmail.com
|
134
|
+
executables: []
|
135
|
+
extensions: []
|
136
|
+
extra_rdoc_files: []
|
137
|
+
files:
|
138
|
+
- ".gitignore"
|
139
|
+
- ".travis.yml"
|
140
|
+
- Gemfile
|
141
|
+
- LICENSE.txt
|
142
|
+
- README.md
|
143
|
+
- Rakefile
|
144
|
+
- acorn_cache.gemspec
|
145
|
+
- bin/console
|
146
|
+
- bin/setup
|
147
|
+
- lib/acorn_cache.rb
|
148
|
+
- lib/acorn_cache/app_exception.rb
|
149
|
+
- lib/acorn_cache/cache_control_header.rb
|
150
|
+
- lib/acorn_cache/cache_controller.rb
|
151
|
+
- lib/acorn_cache/cache_maintenance.rb
|
152
|
+
- lib/acorn_cache/cache_reader.rb
|
153
|
+
- lib/acorn_cache/cache_writer.rb
|
154
|
+
- lib/acorn_cache/cached_response.rb
|
155
|
+
- lib/acorn_cache/config.rb
|
156
|
+
- lib/acorn_cache/freshness_rules.rb
|
157
|
+
- lib/acorn_cache/request.rb
|
158
|
+
- lib/acorn_cache/server_response.rb
|
159
|
+
- lib/acorn_cache/storage.rb
|
160
|
+
- lib/acorn_cache/version.rb
|
161
|
+
homepage: https://github.com/acorncache/acorn-cache
|
162
|
+
licenses:
|
163
|
+
- MIT
|
164
|
+
metadata: {}
|
165
|
+
post_install_message:
|
166
|
+
rdoc_options: []
|
167
|
+
require_paths:
|
168
|
+
- lib
|
169
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
requirements: []
|
180
|
+
rubyforge_project:
|
181
|
+
rubygems_version: 2.4.7
|
182
|
+
signing_key:
|
183
|
+
specification_version: 4
|
184
|
+
summary: A HTTP proxy caching library for Rack apps
|
185
|
+
test_files: []
|
186
|
+
has_rdoc:
|