require 'fileutils' module ActionController #:nodoc: module Caching #:nodoc: def self.append_features(base) super base.send(:include, Pages) base.send(:include, Fragments) end # Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server # can serve without going through the Action Pack. This can be as much as 100 times faster than going the process of dynamically # generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors # are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit # for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates. # # Specifying which actions to cach is done through the caches class method: # # class WeblogController < ActionController::Base # caches :show, :new # end # # This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic # 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 # the Action Pack to generate it. # # Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache # is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends: # # class WeblogController < ActionController::Base # def update # List.update(@params["list"]["id"], @params["list"]) # expire_page :action => "show", :id => @params["list"]["id"] # redirect_to :action => "show", :id => @params["list"]["id"] # end # end # # Additionally, you can expire caches -- or even record new caches -- from outside of the controller, such as from a Active # Record observer: # # class PostObserver < ActiveRecord::Observer # def after_update(post) # WeblogController.expire_page "/weblog/show/#{post.id}" # end # end module Pages def self.append_features(base) super base.extend(ClassMethods) base.class_eval do @@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : "" cattr_accessor :page_cache_directory end end module ClassMethods def cache_page(content, path) FileUtils.makedirs(File.dirname(page_cache_directory + path)) File.open(page_cache_directory + path, "w+") { |f| f.write(content) } logger.info "Cached page: #{path}" unless logger.nil? end def expire_page(path) File.delete(page_cache_directory + path) if File.exists?(page_cache_directory + path) logger.info "Expired page: #{path}" unless logger.nil? end def caches(*actions) actions.each do |action| class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }" end end end def expire_page(options = {}) self.class.expire_page(url_for(options.merge({ :only_path => true }))) end # Expires more than one page at the time. Example: # expire_pages( # { :controller => "lists", :action => "public", :id => list_id }, # { :controller => "lists", :action => "show", :id => list_id } # ) def expire_pages(*options) options.each { |option| expire_page(option) } end def cache_page(content = nil, options = {}) self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true }))) end end module Fragments def self.append_features(base) super base.class_eval do @@cache_store = MemoryStore.new cattr_accessor :fragment_cache_store end end def cache_fragment(binding, name, key = nil) buffer = eval("_erbout", binding) if cache = fragment_cache_store.read(name, key) buffer.concat(cache) logger.info "Fragment hit: #{name}/#{key}" unless logger.nil? else pos = buffer.length yield fragment_cache_store.write(name, key, buffer[pos..-1]) logger.info "Cached fragment: #{name}/#{key}" unless logger.nil? end end def expire_fragment(name, key = nil) fragment_cache_store.delete(name, key) logger.info "Expired fragment: #{name}/#{key}" unless logger.nil? end class MemoryStore def initialize @data = { } end def read(name, key) begin key ? @data[name][key] : @data[name] rescue nil end end def write(name, key, value) if key @data[name] ||= {} @data[name][key] = value else @data[name] = value end end def delete(name, key) key ? @data[name].delete(key) : @data.delete(name) end end class DRbStore < MemoryStore def initialize(address = 'druby://localhost:9192') @data = DRbObject.new(nil, address) end end class FileStore def initialize(cache_path) @cache_path = cache_path end def write(name, key, value) ensure_cache_path(File.dirname(cache_file_path(name, key))) File.open(cache_file_path(name, key), "w+") { |f| f.write(value) } end def read(name, key) begin IO.read(cache_file_path(name, key)) rescue nil end end def delete(name, key) File.delete(cache_file_path(name, key)) if File.exist?(cache_file_path(name, key)) end private def cache_file_path(name, key) key ? "#{@cache_path}/#{name}/#{key}" : "#{@cache_path}/#{name}" end def ensure_cache_path(path) FileUtils.makedirs(path) unless File.exists?(path) end end end end end