benjaminjackson-sinatra-cache 0.3.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +16 -0
- data/LICENSE +20 -0
- data/README.rdoc +417 -0
- data/Rakefile +89 -0
- data/VERSION +1 -0
- data/lib/sinatra/cache.rb +13 -0
- data/lib/sinatra/cache/helpers.rb +667 -0
- data/lib/sinatra/templates.rb +64 -0
- data/sinatra-cache.gemspec +73 -0
- data/spec/fixtures/apps/base/views/css.sass +2 -0
- data/spec/fixtures/apps/base/views/fragments.erb +11 -0
- data/spec/fixtures/apps/base/views/fragments_shared.erb +11 -0
- data/spec/fixtures/apps/base/views/index.erb +1 -0
- data/spec/fixtures/apps/base/views/layout.erb +9 -0
- data/spec/fixtures/apps/base/views/params.erb +3 -0
- data/spec/fixtures/apps/partials/views/home/index.erb +4 -0
- data/spec/fixtures/apps/partials/views/layout.erb +11 -0
- data/spec/fixtures/apps/partials/views/shared/_header.erb +3 -0
- data/spec/sinatra/cache_spec.rb +967 -0
- data/spec/spec_helper.rb +66 -0
- metadata +114 -0
data/Rakefile
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "sinatra-cache"
|
8
|
+
gem.summary = %Q{A Sinatra Extension that makes Page and Fragment Caching easy.}
|
9
|
+
gem.description = %Q{A Sinatra Extension that makes Page and Fragment Caching easy.}
|
10
|
+
gem.email = "kematzy@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/kematzy/sinatra-cache"
|
12
|
+
gem.authors = ["kematzy"]
|
13
|
+
gem.add_dependency('sinatra', '>=1.1.0')
|
14
|
+
gem.add_dependency('sinatra-outputbuffer', '>=0.1.0')
|
15
|
+
gem.add_development_dependency "sinatra-tests", ">= 0.1.6"
|
16
|
+
gem.add_development_dependency "rspec", ">= 1.3.0"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
namespace :spec do
|
38
|
+
|
39
|
+
desc "Run all specifications quietly"
|
40
|
+
Spec::Rake::SpecTask.new(:quiet) do |t|
|
41
|
+
t.libs << "lib"
|
42
|
+
t.spec_opts = ["--color", "--require", "spec/spec_helper.rb"]
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Run specific spec verbosely (SPEC=/path/2/file)"
|
46
|
+
Spec::Rake::SpecTask.new(:select) do |t|
|
47
|
+
t.libs << "lib"
|
48
|
+
t.spec_files = [ENV["SPEC"]]
|
49
|
+
t.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
task :spec => :check_dependencies
|
55
|
+
|
56
|
+
task :default => :spec
|
57
|
+
|
58
|
+
require 'rake/rdoctask'
|
59
|
+
Rake::RDocTask.new do |rdoc|
|
60
|
+
version = File.exist?('VERSION') ? IO.read('VERSION').chomp : "[Unknown]"
|
61
|
+
|
62
|
+
rdoc.rdoc_dir = 'rdoc'
|
63
|
+
rdoc.title = "Sinatra::Cache v#{version}"
|
64
|
+
rdoc.rdoc_files.include('README*')
|
65
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'Build the rdoc HTML Files'
|
69
|
+
task :docs do
|
70
|
+
version = File.exist?('VERSION') ? IO.read('VERSION').chomp : "[Unknown]"
|
71
|
+
|
72
|
+
sh "sdoc -N --title 'Sinatra::Cache v#{version}' lib/ README.rdoc"
|
73
|
+
end
|
74
|
+
|
75
|
+
namespace :docs do
|
76
|
+
|
77
|
+
desc 'Remove rdoc products'
|
78
|
+
task :remove => [:clobber_rdoc]
|
79
|
+
|
80
|
+
desc 'Force a rebuild of the RDOC files'
|
81
|
+
task :rebuild => [:rerdoc]
|
82
|
+
|
83
|
+
desc 'Build docs, and open in browser for viewing (specify BROWSER)'
|
84
|
+
task :open => [:docs] do
|
85
|
+
browser = ENV["BROWSER"] || "safari"
|
86
|
+
sh "open -a #{browser} doc/index.html"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.8
|
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
require 'sinatra/outputbuffer'
|
3
|
+
|
4
|
+
module Sinatra
|
5
|
+
module Cache
|
6
|
+
VERSION = '0.3.8' unless const_defined?(:VERSION)
|
7
|
+
def self.version; "Sinatra::Cache v#{VERSION}"; end
|
8
|
+
end #/ Cache
|
9
|
+
end #/ Sinatra
|
10
|
+
|
11
|
+
%w(templates cache/helpers).each do |lib|
|
12
|
+
require "sinatra/#{lib}"
|
13
|
+
end
|
@@ -0,0 +1,667 @@
|
|
1
|
+
|
2
|
+
module Sinatra
|
3
|
+
|
4
|
+
# = Sinatra::Cache
|
5
|
+
#
|
6
|
+
# A Sinatra Extension that makes Page and Fragment Caching easy wthin your Sinatra apps.
|
7
|
+
#
|
8
|
+
# == Installation
|
9
|
+
#
|
10
|
+
# # Add RubyGems.org (former Gemcutter) to your RubyGems sources
|
11
|
+
# $ gem sources -a http://rubygems.org
|
12
|
+
#
|
13
|
+
# $ (sudo)? gem install sinatra-cache
|
14
|
+
#
|
15
|
+
# == Dependencies
|
16
|
+
#
|
17
|
+
# This Gem depends upon the following:
|
18
|
+
#
|
19
|
+
# === Runtime:
|
20
|
+
#
|
21
|
+
# * sinatra ( >= 1.0.a )
|
22
|
+
# * sinatra-outputbuffer[http://github.com/kematzy/sinatra-outputbuffer] (>= 0.1.0)
|
23
|
+
#
|
24
|
+
# Optionals:
|
25
|
+
#
|
26
|
+
# * sinatra-settings[http://github.com/kematzy/sinatra-settings] (>= 0.1.1) # to view default settings in a browser display.
|
27
|
+
#
|
28
|
+
# === Development & Tests:
|
29
|
+
#
|
30
|
+
# * rspec (>= 1.3.0 )
|
31
|
+
# * rack-test (>= 0.5.3)
|
32
|
+
# * rspec_hpricot_matchers (>= 0.1.0)
|
33
|
+
# * sinatra-tests (>= 0.1.6)
|
34
|
+
# * fileutils
|
35
|
+
# * sass
|
36
|
+
# * ostruct
|
37
|
+
# * yaml
|
38
|
+
# * json
|
39
|
+
#
|
40
|
+
#
|
41
|
+
# == Getting Started
|
42
|
+
#
|
43
|
+
# To start caching your app's ouput, just require and register
|
44
|
+
# the extension in your sub-classed Sinatra app:
|
45
|
+
#
|
46
|
+
# require 'sinatra/cache'
|
47
|
+
#
|
48
|
+
# class YourApp < Sinatra::Base
|
49
|
+
#
|
50
|
+
# # NB! you need to set the root of the app first
|
51
|
+
# set :root, '/path/2/the/root/of/your/app'
|
52
|
+
#
|
53
|
+
# register(Sinatra::Cache)
|
54
|
+
#
|
55
|
+
# set :cache_enabled, true # turn it on
|
56
|
+
#
|
57
|
+
# <snip...>
|
58
|
+
#
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
#
|
62
|
+
# That's more or less it.
|
63
|
+
#
|
64
|
+
# You should now be caching your output by default, in <tt>:production</tt> mode, as long as you use
|
65
|
+
# one of Sinatra's render methods:
|
66
|
+
#
|
67
|
+
# erb(), erubis(), haml(), sass(), builder(), etc..
|
68
|
+
#
|
69
|
+
# ...or any render method that uses <tt>Sinatra::Templates#render()</tt> as its base.
|
70
|
+
#
|
71
|
+
#
|
72
|
+
#
|
73
|
+
# == Configuration Settings
|
74
|
+
#
|
75
|
+
# The default settings should help you get moving quickly, and are fairly common sense based.
|
76
|
+
#
|
77
|
+
#
|
78
|
+
# === <tt>:cache_enabled</tt>
|
79
|
+
#
|
80
|
+
# This setting toggles the cache functionality On / Off.
|
81
|
+
# Default is: <tt>false</tt>
|
82
|
+
#
|
83
|
+
#
|
84
|
+
# === <tt>:cache_environment</tt>
|
85
|
+
#
|
86
|
+
# Sets the environment during which the cache functionality is active.
|
87
|
+
# Default is: <tt>:production</tt>
|
88
|
+
#
|
89
|
+
#
|
90
|
+
# === <tt>:cache_page_extension</tt>+
|
91
|
+
#
|
92
|
+
# Sets the default file extension for cached files.
|
93
|
+
# Default is: <tt>.html</tt>
|
94
|
+
#
|
95
|
+
#
|
96
|
+
# === <tt>:cache_output_dir</tt>
|
97
|
+
#
|
98
|
+
# Sets cache directory where the cached files are stored.
|
99
|
+
# Default is: == "/path/2/your/app/public"
|
100
|
+
#
|
101
|
+
# Although you can set it to the more ideal '<tt>..public/system/cache/</tt>'
|
102
|
+
# if you can get that to work with your webserver setup.
|
103
|
+
#
|
104
|
+
#
|
105
|
+
# === <tt>:cache_fragments_output_dir</tt>
|
106
|
+
#
|
107
|
+
# Sets the directory where cached fragments are stored.
|
108
|
+
# Default is the '../tmp/cache_fragments/' directory at the root of your app.
|
109
|
+
#
|
110
|
+
# This is for security reasons since you don't really want your cached fragments publically available.
|
111
|
+
#
|
112
|
+
#
|
113
|
+
# === <tt>:cache_fragments_wrap_with_html_comments</tt>
|
114
|
+
#
|
115
|
+
# This setting toggles the wrapping of cached fragments in HTML comments. (see below)
|
116
|
+
# Default is: <tt>true</tt>
|
117
|
+
#
|
118
|
+
#
|
119
|
+
# === <tt>:cache_logging</tt>
|
120
|
+
#
|
121
|
+
# This setting toggles the logging of various cache calls. If the app has access to the <tt>#logger</tt> method,
|
122
|
+
# curtesy of Sinatra::Logger[http://github.com/kematzy/sinatra-logger] then it will log there, otherwise logging
|
123
|
+
# is silent.
|
124
|
+
#
|
125
|
+
# Default is: <tt>true</tt>
|
126
|
+
#
|
127
|
+
#
|
128
|
+
# === <tt>:cache_logging_level</tt>
|
129
|
+
#
|
130
|
+
# Sets the level at which the cache logger should log it's messages.
|
131
|
+
# Default is: <tt>:info</tt>
|
132
|
+
#
|
133
|
+
# Available options are: [:fatal, :error, :warn, :info, :debug]
|
134
|
+
#
|
135
|
+
#
|
136
|
+
# == Basic Page Caching
|
137
|
+
#
|
138
|
+
# By default caching only happens in <tt>:production</tt> mode, and via the Sinatra render methods, erb(), etc,
|
139
|
+
#
|
140
|
+
# So asuming we have the following setup (continued from above)
|
141
|
+
#
|
142
|
+
#
|
143
|
+
# class YourApp
|
144
|
+
#
|
145
|
+
# <snip...>
|
146
|
+
#
|
147
|
+
# set :cache_output_dir, "/full/path/2/app/root/public/system/cache"
|
148
|
+
#
|
149
|
+
# <snip...>
|
150
|
+
#
|
151
|
+
# get('/') { erb(:index) } # => is cached as '../index.html'
|
152
|
+
#
|
153
|
+
# get('/contact') { erb(:contact) } # => is cached as '../contact.html'
|
154
|
+
#
|
155
|
+
# # NB! the trailing slash on the URL
|
156
|
+
# get('/about/') { erb(:about) } # => is cached as '../about/index.html'
|
157
|
+
#
|
158
|
+
# get('/feed.rss') { builder(:feed) } # => is cached as '../feed.rss'
|
159
|
+
# # NB! uses the extension of the passed URL,
|
160
|
+
# # but DOES NOT ensure the format of the content based on the extension provided.
|
161
|
+
#
|
162
|
+
# # complex URL with multiple possible params
|
163
|
+
# get %r{/articles/?([\s\w-]+)?/?([\w-]+)?/?([\w-]+)?/?([\w-]+)?/?([\w-]+)?/?([\w-]+)?} do
|
164
|
+
# erb(:articles)
|
165
|
+
# end
|
166
|
+
# # with the '/articles/a/b/c => is cached as ../articles/a/b/c.html
|
167
|
+
#
|
168
|
+
# # NB! the trailing slash on the URL
|
169
|
+
# # with the '/articles/a/b/c/ => is cached as ../articles/a/b/c/index.html
|
170
|
+
#
|
171
|
+
# # CSS caching via Sass # => is cached as '.../css/screen.css'
|
172
|
+
# get '/css/screen.css' do
|
173
|
+
# content_type 'text/css'
|
174
|
+
# sass(:'css/screen')
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# # to turn off caching on certain pages.
|
178
|
+
# get('/dont/cache/this/page') { erb(:aview, :cache => false) } # => is NOT cached
|
179
|
+
#
|
180
|
+
#
|
181
|
+
# # NB! any query string params - [ /?page=X&id=y ] - are stripped off and TOTALLY IGNORED
|
182
|
+
# # during the caching process.
|
183
|
+
#
|
184
|
+
# end
|
185
|
+
#
|
186
|
+
# OK, that's about all you need to know about basic Page Caching right there. Read the above example
|
187
|
+
# carefully until you understand all the variations.
|
188
|
+
#
|
189
|
+
#
|
190
|
+
# == Fragment Caching
|
191
|
+
#
|
192
|
+
# If you just need to cache a fragment of a page, then you would do as follows:
|
193
|
+
#
|
194
|
+
# class YourApp
|
195
|
+
#
|
196
|
+
# set :cache_fragments_output_dir, "/full/path/2/fragments/store/location"
|
197
|
+
#
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# Then in your views / layouts add the following:
|
201
|
+
#
|
202
|
+
# <% cache_fragment(:name_of_fragment) do %>
|
203
|
+
# # do something worth caching
|
204
|
+
# <% end %>
|
205
|
+
#
|
206
|
+
#
|
207
|
+
# Each fragment is stored in the same directory structure as your request
|
208
|
+
# so, if you have a request like this:
|
209
|
+
#
|
210
|
+
# get '/articles/2010/02' ...
|
211
|
+
#
|
212
|
+
# ...the cached fragment will be stored as:
|
213
|
+
#
|
214
|
+
# ../tmp/cache_fragments/articles/2010/02/< name_of_fragment >.html
|
215
|
+
#
|
216
|
+
# This enables you to use similar names for your fragments or have
|
217
|
+
# multiple URLs use the same view / layout.
|
218
|
+
#
|
219
|
+
#
|
220
|
+
# === An important limitation
|
221
|
+
#
|
222
|
+
# The fragment caching is dependent upon the final URL, so in the case of
|
223
|
+
# a blog, where each article uses the same view, but through different URLs,
|
224
|
+
# each of the articles would cache it's own fragment, which is ineffecient.
|
225
|
+
#
|
226
|
+
# To sort-of deal with this limitation I have temporarily added a very hackish
|
227
|
+
# 'fix' through adding a 2nd parameter (see example below), which will remove the
|
228
|
+
# last part of the URL and use the rest of the URL as the stored fragment path.
|
229
|
+
#
|
230
|
+
# So given the URL:
|
231
|
+
#
|
232
|
+
# get '/articles/2010/02/fragment-caching-with-sinatra-cache' ...
|
233
|
+
#
|
234
|
+
# and the following <tt>#cache_fragment</tt> declaration in your view
|
235
|
+
#
|
236
|
+
# <% cache_fragment(:name_of_fragment, :shared) do %>
|
237
|
+
# # do something worth caching
|
238
|
+
# <% end %>
|
239
|
+
#
|
240
|
+
# ...the cached fragment would be stored as:
|
241
|
+
#
|
242
|
+
# ../tmp/cache_fragments/articles/2010/02/< name_of_fragment >.html
|
243
|
+
#
|
244
|
+
# Any other URLs with the same URL root, like...
|
245
|
+
#
|
246
|
+
# get '/articles/2010/02/writing-sinatra-extensions' ...
|
247
|
+
#
|
248
|
+
# ... would use the same cached fragment.
|
249
|
+
#
|
250
|
+
#
|
251
|
+
# <b>NB!</b> currently only supports one level, but Your fork might fix that ;-)
|
252
|
+
#
|
253
|
+
#
|
254
|
+
# == Cache Expiration
|
255
|
+
#
|
256
|
+
# <b>Under development, and not entirely final.</b> See Todo's below for more info.
|
257
|
+
#
|
258
|
+
#
|
259
|
+
# To expire a cached item - file or fragment - you use the :cache_expire() method.
|
260
|
+
#
|
261
|
+
#
|
262
|
+
# cache_expire('/contact') => expires ../contact.html
|
263
|
+
#
|
264
|
+
#
|
265
|
+
# # NB! notice the trailing slash
|
266
|
+
# cache_expire('/contact/') => expires ../contact/index.html
|
267
|
+
#
|
268
|
+
#
|
269
|
+
# cache_expire('/feed.rss') => expires ../feed.rss
|
270
|
+
#
|
271
|
+
#
|
272
|
+
# To expire a cached fragment:
|
273
|
+
#
|
274
|
+
# cache_expire('/some/path', :fragment => :name_of_fragment )
|
275
|
+
#
|
276
|
+
# => expires ../some/path/:name_of_fragment.html
|
277
|
+
#
|
278
|
+
#
|
279
|
+
#
|
280
|
+
# == A few important points to consider
|
281
|
+
#
|
282
|
+
#
|
283
|
+
# === The DANGERS of URL query string params
|
284
|
+
#
|
285
|
+
# By default the caching ignores the query string params, but that's not the only problem with query params.
|
286
|
+
#
|
287
|
+
# Let's say you have a URL like this:
|
288
|
+
#
|
289
|
+
# /products/?product_id=111
|
290
|
+
#
|
291
|
+
# and then inside that template [ .../views/products.erb ], you use the <tt>params[:product_id]</tt>
|
292
|
+
# param passed in for some purpose.
|
293
|
+
#
|
294
|
+
# <ul>
|
295
|
+
# <li>Product ID: <%= params[:product_id] %></li> # => 111
|
296
|
+
# ...
|
297
|
+
# </ul>
|
298
|
+
#
|
299
|
+
# If you cache this URL, then the cached file [ ../cache/products.html ] will be stored with that
|
300
|
+
# value embedded. Obviously not ideal for any other similar URLs with different <tt>product_id</tt>'s
|
301
|
+
#
|
302
|
+
# To overcome this issue, use either of these two methods.
|
303
|
+
#
|
304
|
+
# # in your_app.rb
|
305
|
+
#
|
306
|
+
# # turning off caching on this page
|
307
|
+
#
|
308
|
+
# get '/products/' do
|
309
|
+
# ...
|
310
|
+
# erb(:products, :cache => false)
|
311
|
+
# end
|
312
|
+
#
|
313
|
+
# # or
|
314
|
+
#
|
315
|
+
# # rework the URLs to something like '/products/111 '
|
316
|
+
#
|
317
|
+
# get '/products/:product_id' do
|
318
|
+
# ...
|
319
|
+
# erb(:products)
|
320
|
+
# end
|
321
|
+
#
|
322
|
+
#
|
323
|
+
#
|
324
|
+
# Thats's about all the information you need to know.
|
325
|
+
#
|
326
|
+
#
|
327
|
+
module Cache
|
328
|
+
|
329
|
+
module Helpers
|
330
|
+
|
331
|
+
##
|
332
|
+
# This method either caches the code fragment and then renders it,
|
333
|
+
# or locates the cached fragement and renders that.
|
334
|
+
#
|
335
|
+
# By default the cached fragement is stored in the ../tmp/cache_fragments/
|
336
|
+
# directory at the root of your app.
|
337
|
+
#
|
338
|
+
# Each fragment is stored in the same directory structure as your request
|
339
|
+
# so, if you have a request like this:
|
340
|
+
#
|
341
|
+
# get '/articles/2010/02' ...
|
342
|
+
#
|
343
|
+
# ...the cached fragment will be stored as:
|
344
|
+
#
|
345
|
+
# ../tmp/cache_fragments/articles/2010/02/< name_of_fragment >.html
|
346
|
+
#
|
347
|
+
# This enables you to use similar names for your fragments or have
|
348
|
+
# multiple URLs use the same view / layout.
|
349
|
+
#
|
350
|
+
# ==== Examples
|
351
|
+
#
|
352
|
+
# <% cache_fragment(:name_of_fragment) do %>
|
353
|
+
# # do something worth caching
|
354
|
+
# <% end %>
|
355
|
+
#
|
356
|
+
# === GOTCHA
|
357
|
+
#
|
358
|
+
# The fragment caching is dependent upon the final URL, so in the case of
|
359
|
+
# a blog, where each article uses the same view, but through different URLs,
|
360
|
+
# each of the articles would cache it's own fragment.
|
361
|
+
#
|
362
|
+
# To sort-of deal with this limitation I have added a very hackish 'fix'
|
363
|
+
# through adding a 2nd parameter (see example below), which will
|
364
|
+
# remove the last part of the URL and use the rest of the URL as
|
365
|
+
# the stored fragment path.
|
366
|
+
#
|
367
|
+
# ==== Example
|
368
|
+
#
|
369
|
+
# Given the URL:
|
370
|
+
#
|
371
|
+
# get '/articles/2010/02/fragment-caching-with-sinatra-cache' ...
|
372
|
+
#
|
373
|
+
# and the following <tt>#cache_fragment</tt> declaration in your view
|
374
|
+
#
|
375
|
+
# <% cache_fragment(:name_of_fragment, :shared) do %>
|
376
|
+
# # do something worth caching
|
377
|
+
# <% end %>
|
378
|
+
#
|
379
|
+
# ...the cached fragment would be stored as:
|
380
|
+
#
|
381
|
+
# ../tmp/cache_fragments/articles/2010/02/< name_of_fragment >.html
|
382
|
+
#
|
383
|
+
# Any other URLs with the same URL root, like...
|
384
|
+
#
|
385
|
+
# get '/articles/2010/02/writing-sinatra-extensions' ...
|
386
|
+
#
|
387
|
+
# ... would use the same cached fragment.
|
388
|
+
#
|
389
|
+
# @api public
|
390
|
+
def cache_fragment(fragment_name, shared = nil, &block)
|
391
|
+
# 1. check for a block, there must always be a block
|
392
|
+
raise ArgumentError, "Missing block" unless block_given?
|
393
|
+
|
394
|
+
# 2. get the fragment path, by combining the PATH_INFO of the request, and the fragment_name
|
395
|
+
dir_structure = request.path_info.empty? ? '' : request.path_info.gsub(/^\//,'').gsub(/\/$/,'')
|
396
|
+
# if we are sharing this fragment with other URLs (as in the all the articles in a category of a blog)
|
397
|
+
# then lob off the last part of the URL
|
398
|
+
unless shared.nil?
|
399
|
+
dirs = dir_structure.split('/')
|
400
|
+
dir_structure = dirs.first(dirs.length-1).join('/')
|
401
|
+
end
|
402
|
+
cf = "#{settings.cache_fragments_output_dir}/#{dir_structure}/#{fragment_name}.html"
|
403
|
+
# 3. ensure the fragment cache directory exists for this fragment
|
404
|
+
FileUtils.mkdir_p(File.dirname(cf)) rescue "ERROR: could NOT create the cache directory: [ #{File.dirname(cf)} ]"
|
405
|
+
|
406
|
+
# 3. check if the fragment is already cached ?
|
407
|
+
if test(?f, cf)
|
408
|
+
# 4. yes. cached, so load it up into the ERB buffer . Sorry, don't know how to do this for Haml or any others.
|
409
|
+
block_content = IO.read(cf)
|
410
|
+
else
|
411
|
+
# 4. not cached, so process the block and then cache it
|
412
|
+
block_content = capture_html(&block) if block_given?
|
413
|
+
# 5. add some timestamp comments around the fragment, if the end user wants it
|
414
|
+
if settings.cache_fragments_wrap_with_html_comments
|
415
|
+
content_2_cache = "<!-- cache fragment: #{dir_structure}/#{fragment_name} -->"
|
416
|
+
content_2_cache << block_content
|
417
|
+
content_2_cache << "<!-- /cache fragment: #{fragment_name} cached at [ #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] -->\n"
|
418
|
+
else
|
419
|
+
content_2_cache = block_content
|
420
|
+
end
|
421
|
+
# 6. write it to cache
|
422
|
+
cache_write_file(cf, content_2_cache)
|
423
|
+
end
|
424
|
+
# 5. 'return' the content by
|
425
|
+
block_is_template?(block) ? concat_content(block_content) : block_content
|
426
|
+
end
|
427
|
+
# for future versions once old habits are gone
|
428
|
+
# alias_method :cache, :cache_fragment
|
429
|
+
|
430
|
+
|
431
|
+
##
|
432
|
+
# <b>NB!! Deprecated method.</b>
|
433
|
+
#
|
434
|
+
# Just returns the content after throwing out a warning.
|
435
|
+
#
|
436
|
+
def cache(content, opts={})
|
437
|
+
warn("Deprecated method, caching is now happening by default if the :cache_enabled option is true")
|
438
|
+
content
|
439
|
+
end
|
440
|
+
|
441
|
+
|
442
|
+
##
|
443
|
+
# Expires the cached file (page) or fragment.
|
444
|
+
#
|
445
|
+
# ==== Examples
|
446
|
+
#
|
447
|
+
# cache_expire('/contact') => expires ../contact.html
|
448
|
+
#
|
449
|
+
# cache_expire('/feed.rss') => expires ../feed.rss
|
450
|
+
#
|
451
|
+
# To expire a cached fragment:
|
452
|
+
#
|
453
|
+
# cache_expire('/some/path', :fragment => :name_of_fragment )
|
454
|
+
# => expires ../some/path/:name_of_fragment.html
|
455
|
+
#
|
456
|
+
#
|
457
|
+
# @api public
|
458
|
+
def cache_expire(path, options={})
|
459
|
+
# 1. bail quickly if we don't have caching enabled
|
460
|
+
return unless settings.cache_enabled
|
461
|
+
options = { :fragment => false }.merge(options)
|
462
|
+
|
463
|
+
if options[:fragment] # dealing with a fragment
|
464
|
+
dir_structure = path.gsub(/^\//,'').gsub(/\/$/,'')
|
465
|
+
file_path = "#{settings.cache_fragments_output_dir}/#{dir_structure}/#{options[:fragment]}.html"
|
466
|
+
else
|
467
|
+
file_path = cache_file_path(path)
|
468
|
+
end
|
469
|
+
|
470
|
+
if test(?f, file_path)
|
471
|
+
File.delete(file_path)
|
472
|
+
log(:info,"Expired [#{file_path.sub(settings.root,'')}] successfully")
|
473
|
+
else
|
474
|
+
log(:warn,"The cached file [#{file_path}] could NOT be expired as it was NOT found")
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
|
479
|
+
##
|
480
|
+
# Prints a basic HTML comment with a timestamp in it, so that you can see when a file was cached last.
|
481
|
+
#
|
482
|
+
# *NB!* IE6 does NOT like this to be the first line of a HTML document, so output
|
483
|
+
# inside the <head> tag. Many hours wasted on that lesson ;-)
|
484
|
+
#
|
485
|
+
# ==== Examples
|
486
|
+
#
|
487
|
+
# <%= cache_timestamp %> # => <!-- page cached: 2009-12-21 12:00:00 -->
|
488
|
+
#
|
489
|
+
# @api public
|
490
|
+
def cache_timestamp
|
491
|
+
if settings.cache_enabled && settings.cache_environment == settings.environment
|
492
|
+
"<!-- page cached: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")} -->\n"
|
493
|
+
end
|
494
|
+
end
|
495
|
+
# backwards compat and syntactic sugar for others
|
496
|
+
alias_method :cache_page_timestamp, :cache_timestamp
|
497
|
+
alias_method :page_cached_at, :cache_timestamp
|
498
|
+
|
499
|
+
|
500
|
+
## PRIVATE METHODS
|
501
|
+
private
|
502
|
+
|
503
|
+
##
|
504
|
+
# Converts the PATH_INFO path into the full cached file path.
|
505
|
+
#
|
506
|
+
# ==== GOTCHA:
|
507
|
+
#
|
508
|
+
# <b>NB!</b> completely ignores the URL query params passed such as
|
509
|
+
# in this example:
|
510
|
+
#
|
511
|
+
# /products?page=2
|
512
|
+
#
|
513
|
+
# To capture and cache those query strings, please do as follows:
|
514
|
+
#
|
515
|
+
# get 'products/page/:page' { ... } # in your Sinatra app
|
516
|
+
#
|
517
|
+
# /products/page/2 => .../public/cache/products/page/2.html
|
518
|
+
#
|
519
|
+
#
|
520
|
+
# ==== Examples
|
521
|
+
#
|
522
|
+
# / => .../public/cache/index.html
|
523
|
+
#
|
524
|
+
# /contact => .../public/cache/contact.html
|
525
|
+
#
|
526
|
+
# /contact/ => .../public/cache/contact/index.html
|
527
|
+
#
|
528
|
+
#
|
529
|
+
# @api public
|
530
|
+
def cache_file_path(in_path = nil)
|
531
|
+
path = settings.send(:cache_output_dir).dup
|
532
|
+
|
533
|
+
|
534
|
+
path_info = in_path.nil? ? request.path_info : in_path
|
535
|
+
if (path_info.empty? || path_info == "/" )
|
536
|
+
path << "/index"
|
537
|
+
elsif ( path_info[-1, 1] == '/' )
|
538
|
+
path << ::Rack::Utils.unescape(path_info.chomp('/') << '/index')
|
539
|
+
else
|
540
|
+
path << ::Rack::Utils.unescape(path_info.chomp('/'))
|
541
|
+
end
|
542
|
+
path << settings.cache_page_extension if File.extname(path) == ''
|
543
|
+
return path
|
544
|
+
end
|
545
|
+
|
546
|
+
##
|
547
|
+
# Writes the cached file to disk, only during GET requests,
|
548
|
+
# and then returns the content.
|
549
|
+
#
|
550
|
+
# ==== Examples
|
551
|
+
#
|
552
|
+
#
|
553
|
+
# @api private
|
554
|
+
def cache_write_file(cache_file, content)
|
555
|
+
# only cache GET request [http://rack.rubyforge.org/doc/classes/Rack/Request.html#M000239]
|
556
|
+
if request.get?
|
557
|
+
FileUtils.mkdir_p(File.dirname(cache_file)) rescue "ERROR: could NOT create the cache directory: [ #{File.dirname(cache_file)} ]"
|
558
|
+
File.open(cache_file, 'wb'){ |f| f << content}
|
559
|
+
end
|
560
|
+
return content
|
561
|
+
end
|
562
|
+
|
563
|
+
##
|
564
|
+
# Establishes the file name of the cached file from the path given
|
565
|
+
#
|
566
|
+
# @api private
|
567
|
+
def cache_file_name(path, options={})
|
568
|
+
name = (path.empty? || path == "/") ? "index" : Rack::Utils.unescape(path.sub(/^(\/)/,'').chomp('/'))
|
569
|
+
name << settings.cache_page_extension unless (name.split('/').last || name).include? '.'
|
570
|
+
return name
|
571
|
+
end
|
572
|
+
|
573
|
+
##
|
574
|
+
# Sets the full path to the cached page/file
|
575
|
+
# Dependent upon Sinatra.options .public and .cache_dir variables being present and set.
|
576
|
+
#
|
577
|
+
#
|
578
|
+
# @api private
|
579
|
+
def cache_page_path(path, options={})
|
580
|
+
# test if given a full path rather than relative path, otherwise join the public path to cache_dir
|
581
|
+
# and ensure it is a full path
|
582
|
+
cache_dir = (settings.cache_output_dir == File.expand_path(settings.cache_output_dir)) ?
|
583
|
+
settings.cache_output_dir : File.expand_path("#{settings.public_folder}/#{settings.cache_output_dir}")
|
584
|
+
cache_dir = cache_output_dir[0..-2] if cache_dir[-1,1] == '/'
|
585
|
+
"#{cache_dir}/#{cache_file_name(path, options)}"
|
586
|
+
end
|
587
|
+
|
588
|
+
##
|
589
|
+
# Convenience method that handles logging of Cache related stuff.
|
590
|
+
#
|
591
|
+
# Uses Sinatra::Logger's #logger method if available, otherwise just
|
592
|
+
# puts out the log message.
|
593
|
+
#
|
594
|
+
def log(scope, msg)
|
595
|
+
if settings.cache_logging
|
596
|
+
if scope.to_sym == settings.cache_logging_level.to_sym
|
597
|
+
if self.respond_to?(:logger)
|
598
|
+
logger.send(scope, msg)
|
599
|
+
else
|
600
|
+
puts "#{scope.to_s.upcase}: #{msg}"
|
601
|
+
end
|
602
|
+
end
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
|
607
|
+
end #/ Helpers
|
608
|
+
|
609
|
+
|
610
|
+
##
|
611
|
+
# The default options:
|
612
|
+
#
|
613
|
+
# * +:cache_enabled+ => toggle for the cache functionality. Default is: +false+
|
614
|
+
#
|
615
|
+
# * +:cache_environment+ => sets the environment during which to cache. Default is: +:production+
|
616
|
+
#
|
617
|
+
# * +:cache_page_extension+ => sets the default extension for cached files. Default is: +.html+
|
618
|
+
#
|
619
|
+
# * +:cache_output_dir+ => sets cache directory where cached files are stored. Default is: == "/path/2/your/app/public"
|
620
|
+
# Although you can set it to the more ideal '<tt>..public/system/cache/</tt>'
|
621
|
+
# if you can get that to work with your webserver setup.
|
622
|
+
#
|
623
|
+
# * +:cache_fragments_output_dir+ => sets the directory where cached fragments are stored.
|
624
|
+
# Default is the '../tmp/cache_fragments/' directory at the root of your app.
|
625
|
+
#
|
626
|
+
# * +:cache_fragments_wrap_with_html_comments+ => toggle for wrapping the cached fragment in HTML comments.
|
627
|
+
# Default is: +true+
|
628
|
+
#
|
629
|
+
# * +:cache_logging+ => toggle for logging the cache calls. Default is: +true+
|
630
|
+
#
|
631
|
+
# * +:cache_logging_level+ => sets the level of the cache logger. Default is: <tt>:info</tt>.<br>
|
632
|
+
# Available options are: [:fatal, :error, :warn, :info, :debug]
|
633
|
+
#
|
634
|
+
#
|
635
|
+
def self.registered(app)
|
636
|
+
app.register(Sinatra::OutputBuffer)
|
637
|
+
app.helpers Cache::Helpers
|
638
|
+
|
639
|
+
## CONFIGURATIONS::
|
640
|
+
app.set :cache_enabled, false
|
641
|
+
app.set :cache_environment, :production
|
642
|
+
app.set :cache_page_extension, '.html'
|
643
|
+
app.set :cache_output_dir, lambda { app.public_folder }
|
644
|
+
app.set :cache_fragments_output_dir, lambda { "#{app.root}/tmp/cache_fragments" }
|
645
|
+
app.set :cache_fragments_wrap_with_html_comments, true
|
646
|
+
|
647
|
+
app.set :cache_logging, true
|
648
|
+
app.set :cache_logging_level, :info
|
649
|
+
|
650
|
+
|
651
|
+
## add the extension specific options to those inspectable by :settings_inspect method
|
652
|
+
if app.respond_to?(:sinatra_settings_for_inspection)
|
653
|
+
%w( cache_enabled cache_environment cache_page_extension cache_output_dir
|
654
|
+
cache_fragments_output_dir cache_fragments_wrap_with_html_comments
|
655
|
+
cache_logging cache_logging_level
|
656
|
+
).each do |m|
|
657
|
+
app.sinatra_settings_for_inspection << m
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
end #/ self.registered
|
662
|
+
|
663
|
+
end #/ Cache
|
664
|
+
|
665
|
+
register(Sinatra::Cache) # support classic apps
|
666
|
+
|
667
|
+
end #/ Sinatra
|