sinatra-cache 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +14 -7
- data/LICENSE +1 -1
- data/README.rdoc +394 -0
- data/Rakefile +66 -23
- data/VERSION +1 -0
- data/lib/sinatra/cache.rb +10 -136
- data/lib/sinatra/cache/helpers.rb +663 -0
- data/lib/sinatra/output.rb +147 -0
- data/lib/sinatra/templates.rb +55 -0
- data/sinatra-cache.gemspec +30 -20
- 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/{test/fixtures → spec/fixtures/apps/base}/views/index.erb +0 -0
- data/spec/fixtures/apps/base/views/layout.erb +9 -0
- data/spec/fixtures/apps/base/views/params.erb +3 -0
- data/spec/sinatra/cache_spec.rb +593 -0
- data/spec/spec_helper.rb +62 -0
- metadata +73 -24
- data/README.md +0 -121
- data/VERSION.yml +0 -4
- data/test/SPECS.rdoc +0 -95
- data/test/cache_test.rb +0 -434
- data/test/fixtures/classic.rb +0 -25
- data/test/fixtures/myapp.rb +0 -36
- data/test/fixtures/myapp_default.rb +0 -37
- data/test/helper.rb +0 -62
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
data/lib/sinatra/cache.rb
CHANGED
@@ -1,139 +1,13 @@
|
|
1
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|