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/.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
|