aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/caching.rb
blob: 45d162d2becf3cbaa44c984eeede542d1296b0a2 (plain) (tree)

























































































































































































                                                                                                                                      
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 <tt>caches</tt> 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