actionpack 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (40) hide show
  1. data/CHANGELOG +78 -0
  2. data/README +1 -1
  3. data/install.rb +4 -2
  4. data/lib/action_controller.rb +3 -0
  5. data/lib/action_controller/base.rb +16 -8
  6. data/lib/action_controller/caching.rb +401 -0
  7. data/lib/action_controller/cgi_process.rb +2 -1
  8. data/lib/action_controller/cookies.rb +3 -3
  9. data/lib/action_controller/filters.rb +94 -24
  10. data/lib/action_controller/layout.rb +63 -21
  11. data/lib/action_controller/session/mem_cache_store.rb +1 -1
  12. data/lib/action_controller/support/binding_of_caller.rb +72 -68
  13. data/lib/action_controller/support/breakpoint.rb +526 -524
  14. data/lib/action_controller/support/class_inheritable_attributes.rb +105 -29
  15. data/lib/action_controller/support/core_ext.rb +1 -0
  16. data/lib/action_controller/support/core_ext/hash.rb +5 -0
  17. data/lib/action_controller/support/core_ext/hash/keys.rb +35 -0
  18. data/lib/action_controller/support/core_ext/numeric.rb +7 -0
  19. data/lib/action_controller/support/core_ext/numeric/bytes.rb +33 -0
  20. data/lib/action_controller/support/core_ext/numeric/time.rb +59 -0
  21. data/lib/action_controller/support/core_ext/string.rb +5 -0
  22. data/lib/action_controller/support/core_ext/string/inflections.rb +41 -0
  23. data/lib/action_controller/support/dependencies.rb +1 -14
  24. data/lib/action_controller/support/inflector.rb +6 -6
  25. data/lib/action_controller/support/misc.rb +0 -24
  26. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +7 -4
  27. data/lib/action_controller/test_process.rb +9 -0
  28. data/lib/action_controller/url_rewriter.rb +13 -5
  29. data/lib/action_view/helpers/active_record_helper.rb +10 -3
  30. data/lib/action_view/helpers/cache_helper.rb +10 -0
  31. data/lib/action_view/helpers/form_helper.rb +25 -2
  32. data/lib/action_view/helpers/form_options_helper.rb +1 -2
  33. data/lib/action_view/helpers/url_helper.rb +8 -5
  34. data/lib/action_view/partials.rb +2 -2
  35. data/rakefile +6 -3
  36. data/test/controller/filters_test.rb +118 -3
  37. data/test/controller/render_test.rb +5 -0
  38. data/test/controller/send_file_test.rb +24 -0
  39. data/test/controller/url_test.rb +47 -2
  40. metadata +16 -2
data/CHANGELOG CHANGED
@@ -1,3 +1,81 @@
1
+ *1.3.0* (January 17th, 2005)
2
+
3
+ * Added an extensive caching module that offers three levels of granularity (page, action, fragment) and a variety of stores.
4
+ Read more in ActionController::Caching.
5
+
6
+ * Added the option of passing a block to ActiveRecordHelper#form in order to add more to the auto-generated form #469 [dom@sisna.com]
7
+
8
+ form("entry", :action => "sign") do |form|
9
+ form << content_tag("b", "Department")
10
+ form << collection_select("department", "id", @departments, "id", "name")
11
+ end
12
+
13
+ * Added arrays as a value option for params in url_for and friends #467 [Eric Anderson]. Example:
14
+
15
+ url_for(:controller => 'user', :action => 'delete', :params => { 'username' => %( paul john steve ) } )
16
+ # => /user/delete?username[]=paul&username[]=john&username[]=steve
17
+
18
+ * Fixed that controller tests can now assert on the use of cookies #466 [Alexey]
19
+
20
+ * Fixed that send_file would "remember" all the files sent by adding to the headers again and again #458 [bitsweat]
21
+
22
+ * Fixed url rewriter confusion when the controller or action name was a substring of the controller_prefix or action_prefix
23
+
24
+ * Added conditional layouts like <tt>layout "weblog_standard", :except => :rss</tt> #452 [Marcel Molina]
25
+
26
+ * Fixed that MemCacheStore wasn't included by default and added default MemCache object pointing to localhost #447 [Lucas Carlson]
27
+
28
+ * Added fourth argument to render_collection_of_partials that allows you to specify local_assigns -- just like render_partial #432 [zenspider]
29
+
30
+ * Fixed that host would choke when cgi.host returned nil #432 [Tobias Luetke]
31
+
32
+ * Added that form helpers now take an index option #448 [Tim Bates]
33
+
34
+ Example:
35
+ text_field "person", "name", "index" => 3
36
+
37
+ Becomes:
38
+ <input type="text" name="person[3][name]" id="person_3_name" value="<%= @person.name %>" />
39
+
40
+ * Fixed three issues with retrying breakpoints #417 [Florian Gross]
41
+
42
+ 1. Don't screw up pages that use multiple values for the same parameter (?foo=bar&foo=qux was converted to ?foo=barqux)
43
+ 2. Don't screw up all forms when you click the "Retry with Breakpoint" link multiple times instead of reloading
44
+ (This caused the parameters to be added multiple times for GET forms leading to trouble.)
45
+ 3. Don't add ?BP-RETRY=1 multiple times
46
+
47
+ * Added that all renders and redirects now return false, so they can be used as the last line in before_filters to stop execution.
48
+
49
+ Before:
50
+ def authenticate
51
+ unless @session[:authenticated]
52
+ redirect_to :controller => "account", :action => "login"
53
+ return false
54
+ end
55
+ end
56
+
57
+ After:
58
+ def authenticate
59
+ redirect_to(:controller => "account", :action => "login") unless @session[:authenticated]
60
+ end
61
+
62
+ * Added conditional filters #431 [Marcel]. Example:
63
+
64
+ class JournalController < ActionController::Base
65
+ # only require authentication if the current action is edit or delete
66
+ before_filter :authorize, :only_on => [ :edit, :delete ]
67
+
68
+ private
69
+ def authorize
70
+ # redirect to login unless authenticated
71
+ end
72
+ end
73
+
74
+ * Added Base#render_nothing as a cleaner way of doing render_text "" when you're not interested in returning anything but an empty response.
75
+
76
+ * Added the possibility of passing nil to UrlHelper#link_to to use the link itself as the name
77
+
78
+
1
79
  *1.2.0* (January 4th, 2005)
2
80
 
3
81
  * Added MemCacheStore for storing session data in Danga's MemCache system [Bob Cottrell]
data/README CHANGED
@@ -145,7 +145,7 @@ A short rundown of the major features:
145
145
  Result of running hello_world action:
146
146
  <html><body><h1>Hello world</h1></body></html>
147
147
 
148
- Learn more in link:classes/ActionController/Layout.html
148
+ Learn more in link:classes/ActionController/Layout/ClassMethods.html
149
149
 
150
150
 
151
151
  * Advanced redirection that makes pretty urls easy
data/install.rb CHANGED
@@ -19,7 +19,7 @@ unless $sitedir
19
19
  end
20
20
 
21
21
  makedirs = %w{ action_controller/assertions action_controller/cgi_ext
22
- action_controller/session action_controller/support
22
+ action_controller/session action_controller/support action_controller/support/core_ext
23
23
  action_controller/templates action_controller/templates/rescues
24
24
  action_controller/templates/scaffolds
25
25
  action_view/helpers action_view/vendor action_view/vendor/builder
@@ -41,6 +41,7 @@ files = %w-
41
41
  action_controller/cgi_ext/cgi_ext.rb
42
42
  action_controller/cgi_ext/cgi_methods.rb
43
43
  action_controller/cgi_ext/cookie_performance_fix.rb
44
+ action_controller/caching.rb
44
45
  action_controller/cgi_process.rb
45
46
  action_controller/cookies.rb
46
47
  action_controller/dependencies.rb
@@ -60,7 +61,8 @@ files = %w-
60
61
  action_controller/support/class_inheritable_attributes.rb
61
62
  action_controller/support/class_attribute_accessors.rb
62
63
  action_controller/support/clean_logger.rb
63
- action_controller/support/cookie_performance_fix.rb
64
+ action_controller/support/core_ext/hash_ext.rb
65
+ action_controller/support/core_ext.rb
64
66
  action_controller/support/inflector.rb
65
67
  action_controller/support/binding_of_caller.rb
66
68
  action_controller/support/breakpoint.rb
@@ -23,6 +23,7 @@
23
23
 
24
24
  $:.unshift(File.dirname(__FILE__))
25
25
 
26
+ require 'action_controller/support/core_ext'
26
27
  require 'action_controller/support/clean_logger'
27
28
  require 'action_controller/support/misc'
28
29
  require 'action_controller/support/dependencies'
@@ -39,6 +40,7 @@ require 'action_controller/scaffolding'
39
40
  require 'action_controller/helpers'
40
41
  require 'action_controller/cookies'
41
42
  require 'action_controller/cgi_process'
43
+ require 'action_controller/caching'
42
44
 
43
45
  ActionController::Base.class_eval do
44
46
  include ActionController::Filters
@@ -51,6 +53,7 @@ ActionController::Base.class_eval do
51
53
  include ActionController::Helpers
52
54
  include ActionController::Cookies
53
55
  include ActionController::Session
56
+ include ActionController::Caching
54
57
  end
55
58
 
56
59
  require 'action_view'
@@ -4,6 +4,7 @@ require 'action_controller/url_rewriter'
4
4
  require 'action_controller/support/class_attribute_accessors'
5
5
  require 'action_controller/support/class_inheritable_attributes'
6
6
  require 'action_controller/support/inflector'
7
+ require 'drb'
7
8
 
8
9
  module ActionController #:nodoc:
9
10
  class ActionControllerError < StandardError #:nodoc:
@@ -177,12 +178,11 @@ module ActionController #:nodoc:
177
178
  DEFAULT_RENDER_STATUS_CODE = "200 OK"
178
179
 
179
180
  DEFAULT_SEND_FILE_OPTIONS = {
180
- :type => 'application/octet_stream',
181
- :disposition => 'attachment',
181
+ :type => 'application/octet_stream'.freeze,
182
+ :disposition => 'attachment'.freeze,
182
183
  :stream => true,
183
184
  :buffer_size => 4096
184
- }
185
-
185
+ }.freeze
186
186
 
187
187
  # Determines whether the view has access to controller internals @request, @response, @session, and @template.
188
188
  # By default, it does.
@@ -359,7 +359,7 @@ module ActionController #:nodoc:
359
359
  # <tt>render_action "show_many"</tt> in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or
360
360
  # "#{template_root}/weblog/show_many.rxml".
361
361
  def render_action(action_name, status = nil) #:doc:
362
- render default_template_name(action_name), status
362
+ render(default_template_name(action_name), status)
363
363
  end
364
364
 
365
365
  # Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be
@@ -389,6 +389,13 @@ module ActionController #:nodoc:
389
389
  @response.headers["Status"] = status || DEFAULT_RENDER_STATUS_CODE
390
390
  @response.body = block_given? ? block : text
391
391
  @performed_render = true
392
+ return false
393
+ end
394
+
395
+ # Renders an empty response that can be used when the request is only interested in triggering an effect. Do note that good
396
+ # HTTP manners mandate that you don't use GET requests to trigger data changes.
397
+ def render_nothing(status = nil)
398
+ render_text "", status
392
399
  end
393
400
 
394
401
  # Sends the file by streaming it 4096 bytes at a time. This way the
@@ -434,7 +441,7 @@ module ActionController #:nodoc:
434
441
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
435
442
  # for the Cache-Control header spec.
436
443
  def send_file(path, options = {}) #:doc:
437
- raise MissingFile, path unless File.file?(path) and File.readable?(path)
444
+ raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
438
445
 
439
446
  options[:length] ||= File.size(path)
440
447
  options[:filename] ||= File.basename(path)
@@ -537,6 +544,7 @@ module ActionController #:nodoc:
537
544
  logger.info("Redirected to #{url}") unless logger.nil?
538
545
  @response.redirect(url, permanently)
539
546
  @performed_redirect = true
547
+ return false
540
548
  end
541
549
 
542
550
  # Resets the session by clearsing out all the objects stored within and initializing a new session object.
@@ -596,7 +604,7 @@ module ActionController #:nodoc:
596
604
 
597
605
  def action_methods
598
606
  action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) }
599
- action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) }
607
+ action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.public_instance_methods(false) }
600
608
  end
601
609
 
602
610
  def add_variables_to_assigns
@@ -656,7 +664,7 @@ module ActionController #:nodoc:
656
664
  raise ArgumentError, ":#{arg} option required" if options[arg].nil?
657
665
  end
658
666
 
659
- disposition = options[:disposition] || 'attachment'
667
+ disposition = options[:disposition].dup || 'attachment'
660
668
  disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
661
669
 
662
670
  @headers.update(
@@ -0,0 +1,401 @@
1
+ require 'fileutils'
2
+
3
+ module ActionController #:nodoc:
4
+ # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
5
+ # around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment.
6
+ #
7
+ # You can read more about each approach and the sweeping assistance by clicking the modules below.
8
+ #
9
+ # Note: To turn off all caching and sweeping, set Base.perform_caching = false.
10
+ module Caching
11
+ def self.append_features(base) #:nodoc:
12
+ super
13
+ base.send(:include, Pages, Actions, Fragments, Sweeping)
14
+ base.class_eval do
15
+ @@perform_caching = true
16
+ cattr_accessor :perform_caching
17
+ end
18
+ end
19
+
20
+ # Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
21
+ # can serve without going through the Action Pack. This can be as much as 100 times faster than going the process of dynamically
22
+ # generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors
23
+ # are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit
24
+ # for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates.
25
+ #
26
+ # Specifying which actions to cach is done through the <tt>caches</tt> class method:
27
+ #
28
+ # class WeblogController < ActionController::Base
29
+ # caches_page :show, :new
30
+ # end
31
+ #
32
+ # This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic
33
+ # generation. This is how the web server is able pick up a cache file when it exists and otherwise let the request pass on to
34
+ # the Action Pack to generate it.
35
+ #
36
+ # Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache
37
+ # is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends:
38
+ #
39
+ # class WeblogController < ActionController::Base
40
+ # def update
41
+ # List.update(@params["list"]["id"], @params["list"])
42
+ # expire_page :action => "show", :id => @params["list"]["id"]
43
+ # redirect_to :action => "show", :id => @params["list"]["id"]
44
+ # end
45
+ # end
46
+ #
47
+ # Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
48
+ # expired.
49
+ #
50
+ # == Setting the cache directory
51
+ #
52
+ # The cache directory should be the document root for the web server and is set using Base.page_cache_directory = "/document/root".
53
+ # For Rails, this directory has already been set to RAILS_ROOT + "/public".
54
+ module Pages
55
+ def self.append_features(base) #:nodoc:
56
+ super
57
+ base.extend(ClassMethods)
58
+ base.class_eval do
59
+ @@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : ""
60
+ cattr_accessor :page_cache_directory
61
+ end
62
+ end
63
+
64
+ module ClassMethods
65
+ # Expires the page that was cached with the +path+ as a key. Example:
66
+ # expire_page "/lists/show"
67
+ def expire_page(path)
68
+ return unless perform_caching
69
+ File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path))
70
+ logger.info "Expired page: #{path}" unless logger.nil?
71
+ end
72
+
73
+ # Manually cache the +content+ in the key determined by +path+. Example:
74
+ # cache_page "I'm the cached content", "/lists/show"
75
+ def cache_page(content, path)
76
+ return unless perform_caching
77
+ FileUtils.makedirs(File.dirname(page_cache_path(path)))
78
+ File.open(page_cache_path(path), "w+") { |f| f.write(content) }
79
+ logger.info "Cached page: #{path}" unless logger.nil?
80
+ end
81
+
82
+ # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that
83
+ # matches the triggering url.
84
+ def caches_page(*actions)
85
+ return unless perform_caching
86
+ actions.each do |action|
87
+ class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
88
+ end
89
+ end
90
+
91
+ private
92
+ def page_cache_path(path)
93
+ if path[-1,1] == '/'
94
+ page_cache_directory + path + '/index'
95
+ else
96
+ page_cache_directory + path
97
+ end
98
+ end
99
+ end
100
+
101
+ # Expires the page that was cached with the +options+ as a key. Example:
102
+ # expire_page :controller => "lists", :action => "show"
103
+ def expire_page(options = {})
104
+ return unless perform_caching
105
+ if options[:action].is_a?(Array)
106
+ options[:action].dup.each do |action|
107
+ self.class.expire_page(url_for(options.merge({ :only_path => true, :action => action })))
108
+ end
109
+ else
110
+ self.class.expire_page(url_for(options.merge({ :only_path => true })))
111
+ end
112
+ end
113
+
114
+ # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used
115
+ # If no options are provided, the current +options+ for this action is used. Example:
116
+ # cache_page "I'm the cached content", :controller => "lists", :action => "show"
117
+ def cache_page(content = nil, options = {})
118
+ return unless perform_caching && caching_allowed
119
+ self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true })))
120
+ end
121
+
122
+ private
123
+ def caching_allowed
124
+ !@request.method.post? && (@request.parameters.reject {|k, v| ['id', 'action', 'controller'].include?(k)}).empty?
125
+ end
126
+ end
127
+
128
+ # Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching,
129
+ # every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which
130
+ # allows for authentication and other restrictions on whether someone are supposed to see the cache. Example:
131
+ #
132
+ # class ListsController < ApplicationController
133
+ # before_filter :authenticate, :except => :public
134
+ # caches_page :public
135
+ # caches_action :show, :feed
136
+ # end
137
+ #
138
+ # In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the
139
+ # show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches.
140
+ #
141
+ # Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both
142
+ # the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named
143
+ # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and
144
+ # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
145
+ module Actions
146
+ def self.append_features(base) #:nodoc:
147
+ super
148
+ base.extend(ClassMethods)
149
+ base.send(:attr_accessor, :rendered_action_cache)
150
+ end
151
+
152
+ module ClassMethods #:nodoc:
153
+ def caches_action(*actions)
154
+ return unless perform_caching
155
+ around_filter(ActionCacheFilter.new(*actions))
156
+ end
157
+ end
158
+
159
+ def expire_action(options = {})
160
+ return unless perform_caching
161
+ if options[:action].is_a?(Array)
162
+ options[:action].dup.each do |action|
163
+ expire_fragment(url_for(options.merge({ :action => action })).split("://").last)
164
+ end
165
+ else
166
+ expire_fragment(url_for(options).split("://").last)
167
+ end
168
+ end
169
+
170
+ class ActionCacheFilter #:nodoc:
171
+ def initialize(*actions)
172
+ @actions = actions
173
+ end
174
+
175
+ def before(controller)
176
+ return unless @actions.include?(controller.action_name.intern)
177
+ if cache = controller.read_fragment(controller.url_for.split("://").last)
178
+ controller.rendered_action_cache = true
179
+ controller.send(:render_text, cache)
180
+ false
181
+ end
182
+ end
183
+
184
+ def after(controller)
185
+ return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
186
+ controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
187
+ end
188
+ end
189
+ end
190
+
191
+ # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
192
+ # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
193
+ # parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like:
194
+ #
195
+ # <b>Hello <%= @name %></b>
196
+ # <% cache(binding) do %>
197
+ # All the topics in the system:
198
+ # <%= render_collection_of_partials "topic", Topic.find_all %>
199
+ # <% end %>
200
+ #
201
+ # This cache will bind to the name of action that called it. So you would be able to invalidate it using
202
+ # <tt>expire_fragment(:controller => "topics", :action => "list")</tt> -- if that was the controller/action used. This is not too helpful
203
+ # if you need to cache multiple fragments per action or if the action itself is cached using <tt>caches_action</tt>. So instead we should
204
+ # qualify the name of the action used with something like:
205
+ #
206
+ # <% cache(binding, :action => "list", :action_suffix => "all_topics") do %>
207
+ #
208
+ # That would result in a name such as "/topics/list/all_topics", which wouldn't conflict with any action cache and neither with another
209
+ # fragment using a different suffix. Note that the URL doesn't have to really exist or be callable. We're just using the url_for system
210
+ # to generate unique cache names that we can refer to later for expirations. The expiration call for this example would be
211
+ # <tt>expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")</tt>.
212
+ #
213
+ # == Fragment stores
214
+ #
215
+ # In order to use the fragment caching, you need to designate where the caches should be stored. This is done by assigning a fragment store
216
+ # of which there are four different kinds:
217
+ #
218
+ # * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and share the fragments for
219
+ # all the web server processes running off the same application directory.
220
+ # * MemoryStore: Keeps the fragments in memory, which is fine for WEBrick and for FCGI (if you don't care that each FCGI process holds its
221
+ # own fragment store). It's not suitable for CGI as the process is thrown away at the end of each request. It can potentially also take
222
+ # up a lot of memory since each process keeps all the caches in memory.
223
+ # * DRbStore: Keeps the fragments in the memory of a separate, shared DRb process. This works for all environments and only keeps one cache
224
+ # around for all processes, but requires that you run and manage a separate DRb process.
225
+ # * MemCachedStore: Works like DRbStore, but uses Danga's MemCached instead.
226
+ #
227
+ # Configuration examples (MemoryStore is the default):
228
+ #
229
+ # ActionController::Base.fragment_cache_store =
230
+ # ActionController::Caching::Fragments::MemoryStore.new
231
+ #
232
+ # ActionController::Base.fragment_cache_store =
233
+ # ActionController::Caching::Fragments::FileStore.new("/path/to/cache/directory")
234
+ #
235
+ # ActionController::Base.fragment_cache_store =
236
+ # ActionController::Caching::Fragments::DRbStore.new("druby://localhost:9192")
237
+ #
238
+ # ActionController::Base.fragment_cache_store =
239
+ # ActionController::Caching::Fragments::FileStore.new("localhost")
240
+ module Fragments
241
+ def self.append_features(base) #:nodoc:
242
+ super
243
+ base.class_eval do
244
+ @@cache_store = MemoryStore.new
245
+ cattr_accessor :fragment_cache_store
246
+ end
247
+ end
248
+
249
+ # Called by CacheHelper#cache
250
+ def cache_erb_fragment(binding, name = {}, options = {})
251
+ unless perform_caching then yield; return end
252
+
253
+ buffer = eval("_erbout", binding)
254
+
255
+ if cache = read_fragment(name, options)
256
+ buffer.concat(cache)
257
+ else
258
+ pos = buffer.length
259
+ yield
260
+ write_fragment(name, buffer[pos..-1], options)
261
+ end
262
+ end
263
+
264
+ def write_fragment(name, content, options = {})
265
+ name = url_for(name).split("://").last if name.is_a?(Hash)
266
+ fragment_cache_store.write(name, content, options)
267
+ logger.info "Cached fragment: #{name}" unless logger.nil?
268
+ content
269
+ end
270
+
271
+ def read_fragment(name, options = {})
272
+ name = url_for(name).split("://").last if name.is_a?(Hash)
273
+ if cache = fragment_cache_store.read(name, options)
274
+ logger.info "Fragment hit: #{name}" unless logger.nil?
275
+ cache
276
+ else
277
+ false
278
+ end
279
+ end
280
+
281
+ def expire_fragment(name, options = {})
282
+ name = url_for(name).split("://").last if name.is_a?(Hash)
283
+ fragment_cache_store.delete(name, options)
284
+ logger.info "Expired fragment: #{name}" unless logger.nil?
285
+ end
286
+
287
+ class MemoryStore #:nodoc:
288
+ def initialize
289
+ @data, @mutex = { }, Mutex.new
290
+ end
291
+
292
+ def read(name, options = {}) #:nodoc:
293
+ begin
294
+ @mutex.synchronize { @data[name] }
295
+ rescue
296
+ nil
297
+ end
298
+ end
299
+
300
+ def write(name, value, options = {}) #:nodoc:
301
+ @mutex.synchronize { @data[name] = value }
302
+ end
303
+
304
+ def delete(name, options = {}) #:nodoc:
305
+ @mutex.synchronize { @data.delete(name) }
306
+ end
307
+ end
308
+
309
+ class DRbStore < MemoryStore #:nodoc:
310
+ def initialize(address = 'druby://localhost:9192')
311
+ @data = DRbObject.new(nil, address)
312
+ end
313
+ end
314
+
315
+ class MemCacheStore < MemoryStore #:nodoc:
316
+ def initialize(address = 'localhost')
317
+ @data = MemCache.new(address)
318
+ end
319
+ end
320
+
321
+ class FileStore #:nodoc:
322
+ def initialize(cache_path)
323
+ @cache_path = cache_path
324
+ end
325
+
326
+ def write(name, value, options = {}) #:nodoc:
327
+ begin
328
+ ensure_cache_path(File.dirname(real_file_path(name)))
329
+ File.open(real_file_path(name), "w+") { |f| f.write(value) }
330
+ rescue => e
331
+ Base.logger.info "Couldn't create cache directory: #{name} (#{e.message})" unless Base.logger.nil?
332
+ end
333
+ end
334
+
335
+ def read(name, options = {}) #:nodoc:
336
+ begin
337
+ IO.read(real_file_path(name))
338
+ rescue
339
+ nil
340
+ end
341
+ end
342
+
343
+ def delete(name, options) #:nodoc:
344
+ File.delete(real_file_path(name)) if File.exist?(real_file_path(name))
345
+ end
346
+
347
+ private
348
+ def real_file_path(name)
349
+ "#{@cache_path}/#{name}"
350
+ end
351
+
352
+ def ensure_cache_path(path)
353
+ FileUtils.makedirs(path) unless File.exists?(path)
354
+ end
355
+ end
356
+ end
357
+
358
+ # Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
359
+ # They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
360
+ #
361
+ # class ListSweeper < ActiveRecord::Observer
362
+ # observe List, Item
363
+ #
364
+ # def after_save(record)
365
+ # @list = record.is_a?(List) ? record : record.list
366
+ # end
367
+ #
368
+ # def filter(controller)
369
+ # controller.expire_page(:controller => "lists", :action => %w( show public feed ), :id => @list.id)
370
+ # controller.expire_action(:controller => "lists", :action => "all")
371
+ # @list.shares.each { |share| controller.expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
372
+ # end
373
+ # end
374
+ #
375
+ # The sweeper is assigned on the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method:
376
+ #
377
+ # class ListsController < ApplicationController
378
+ # caches_action :index, :show, :public, :feed
379
+ # cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
380
+ # end
381
+ #
382
+ # In the example above, four actions are cached and three actions are responsible of expiring those caches.
383
+ module Sweeping
384
+ def self.append_features(base) #:nodoc:
385
+ super
386
+ base.extend(ClassMethods)
387
+ end
388
+
389
+ module ClassMethods #:nodoc:
390
+ def cache_sweeper(*sweepers)
391
+ return unless perform_caching
392
+ configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
393
+ sweepers.each do |sweeper|
394
+ observer(sweeper)
395
+ after_filter(Object.const_get(Inflector.classify(sweeper)).instance, :only => configuration[:only])
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end