sinatra-cache 0.2.3 → 0.3.0

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