require'active_support/core_ext/hash/keys'moduleActionControllermoduleConditionalGetextendActiveSupport::ConcernincludeRackDelegationincludeHeadincludeddoclass_attribute:etaggersself.etaggers=[]endmoduleClassMethods# Allows you to consider additional controller-wide information when generating an ETag.# For example, if you serve pages tailored depending on who's logged in at the moment, you# may want to add the current user id to be part of the ETag to prevent authorized displaying# of cached pages.## class InvoicesController < ApplicationController# etag { current_user.try :id }## def show# # Etag will differ even for the same invoice when it's viewed by a different current_user# @invoice = Invoice.find(params[:id])# fresh_when(@invoice)# end# enddefetag(&etagger)self.etaggers+=[etagger]endend# Sets the +etag+, +last_modified+, or both on the response and renders a# <tt>304 Not Modified</tt> response if the request is already fresh.## === Parameters:## * <tt>:etag</tt>.# * <tt>:last_modified</tt>.# * <tt>:public</tt> By default the Cache-Control header is private, set this to# +true+ if you want your application to be cachable by other devices (proxy caches).# * <tt>:template</tt> By default, the template digest for the current# controller/action is included in ETags. If the action renders a# different template, you can include its digest instead. If the action# doesn't render a template at all, you can pass <tt>template: false</tt># to skip any attempt to check for a template digest.## === Example:## def show# @article = Article.find(params[:id])# fresh_when(etag: @article, last_modified: @article.created_at, public: true)# end## This will render the show template if the request isn't sending a matching ETag or# If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.## You can also just pass a record where +last_modified+ will be set by calling# +updated_at+ and the +etag+ by passing the object itself.## def show# @article = Article.find(params[:id])# fresh_when(@article)# end## When passing a record, you can still set whether the public header:## def show# @article = Article.find(params[:id])# fresh_when(@article, public: true)# end## When rendering a different template than the default controller/action# style, you can indicate which digest to include in the ETag:## before_action { fresh_when @article, template: 'widgets/show' }#deffresh_when(record_or_options,additional_options={})ifrecord_or_options.is_a?Hashoptions=record_or_optionsoptions.assert_valid_keys(:etag,:last_modified,:public,:template)elserecord=record_or_optionsoptions={etag: record,last_modified: record.try(:updated_at)}.merge!(additional_options)endresponse.etag=combine_etags(options)ifoptions[:etag]||options[:template]response.last_modified=options[:last_modified]ifoptions[:last_modified]response.cache_control[:public]=trueifoptions[:public]head:not_modifiedifrequest.fresh?(response)end# Sets the +etag+ and/or +last_modified+ on the response and checks it against# the client request. If the request doesn't match the options provided, the# request is considered stale and should be generated from scratch. Otherwise,# it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.## === Parameters:## * <tt>:etag</tt>.# * <tt>:last_modified</tt>.# * <tt>:public</tt> By default the Cache-Control header is private, set this to# +true+ if you want your application to be cachable by other devices (proxy caches).# * <tt>:template</tt> By default, the template digest for the current# controller/action is included in ETags. If the action renders a# different template, you can include its digest instead. If the action# doesn't render a template at all, you can pass <tt>template: false</tt># to skip any attempt to check for a template digest.## === Example:## def show# @article = Article.find(params[:id])## if stale?(etag: @article, last_modified: @article.created_at)# @statistics = @article.really_expensive_call# respond_to do |format|# # all the supported formats# end# end# end## You can also just pass a record where +last_modified+ will be set by calling# +updated_at+ and the +etag+ by passing the object itself.## def show# @article = Article.find(params[:id])## if stale?(@article)# @statistics = @article.really_expensive_call# respond_to do |format|# # all the supported formats# end# end# end## When passing a record, you can still set whether the public header:## def show# @article = Article.find(params[:id])## if stale?(@article, public: true)# @statistics = @article.really_expensive_call# respond_to do |format|# # all the supported formats# end# end# end## When rendering a different template than the default controller/action# style, you can indicate which digest to include in the ETag:## def show# super if stale? @article, template: 'widgets/show'# end#defstale?(record_or_options,additional_options={})fresh_when(record_or_options,additional_options)!request.fresh?(response)end# Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+# instruction, so that intermediate caches must not cache the response.## expires_in 20.minutes# expires_in 3.hours, public: true# expires_in 3.hours, public: true, must_revalidate: true## This method will overwrite an existing Cache-Control header.# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.## The method will also ensure a HTTP Date header for client compatibility.defexpires_in(seconds,options={})response.cache_control.merge!(:max_age=>seconds,:public=>options.delete(:public),:must_revalidate=>options.delete(:must_revalidate))options.delete(:private)response.cache_control[:extras]=options.map{|k,v|"#{k}=#{v}"}response.date=Time.nowunlessresponse.date?end# Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should# occur by the browser or intermediate caches (like caching proxy servers).defexpires_nowresponse.cache_control.replace(:no_cache=>true)endprivatedefcombine_etags(options)etags=etaggers.map{|etagger|instance_exec(options,&etagger)}.compactetags.unshiftoptions[:etag]endendend