locomotivecms/steam

View on GitHub
lib/locomotive/steam/middlewares/cache.rb

Summary

Maintainability
A
35 mins
Test Coverage
module Locomotive::Steam
  module Middlewares

    class Cache < ThreadSafe

      include Concerns::Helpers

      CACHEABLE_RESPONSE_CODES  = [200, 301, 404, 410].freeze

      CACHEABLE_REQUEST_METHODS = %w(GET HEAD).freeze

      DEFAULT_CACHE_CONTROL     = 'max-age=0, s-maxage=3600, public, must-revalidate'.freeze

      DEFAULT_CACHE_VARY        = 'Accept-Language'.freeze

      NO_CACHE_CONTROL          = 'max-age=0, private, must-revalidate'.freeze

      def _call
        if cacheable?
          key = cache_key

          # TODO: only for debugging
          # log("HTTP keys: #{env.select { |key, _| key.starts_with?('HTTP_') }}".light_blue)

          # Test if the ETag or Last Modified has been modified. If not, return a 304 response
          if stale?(key)
            render_response(nil, 304, nil)
            return
          end

          # we have to tell the CDN (or any proxy) what the expiration & validation strategy are
          env['steam.cache_control']        = cache_control
          env['steam.cache_vary']           = cache_vary
          env['steam.cache_etag']           = key
          env['steam.cache_last_modified']  = site.last_modified_at.httpdate

          # retrieve the response from the cache.
          # This is useful if no CDN is being used.
          code, headers, _ = response = fetch_cached_response(key)

          unless CACHEABLE_RESPONSE_CODES.include?(code.to_i)
            env['steam.cache_control'] = headers['cache-control'] = NO_CACHE_CONTROL
            env['steam.cache_vary'] = headers['Vary'] = nil
          end

          # we don't want to render twice the page
          @next_response = response
        else
          env['steam.cache_control']  = NO_CACHE_CONTROL
        end
      end

      private

      def fetch_cached_response(key)
        log("Cache key = #{key.inspect}")
        if marshaled = cache.read(key)
          log('Cache HIT')
          Marshal.load(marshaled)
        else
          log('Cache MISS')
          self.next.tap do |response|
            # cache the HTML for further validations (+ optimization)
            cache.write(key, marshal(response))
          end
        end
      end

      def cacheable?
        CACHEABLE_REQUEST_METHODS.include?(env['REQUEST_METHOD']) &&
        !live_editing? &&
        site.try(:cache_enabled) &&
        page.try(:cache_enabled) &&
        is_redirect_url?
      end

      def cache_key
        site, path, query = env['steam.site'], env['PATH_INFO'], env['QUERY_STRING']
        key = "#{Locomotive::Steam::VERSION}/site/#{site._id}/#{site.last_modified_at.to_i}/page/#{path}/#{query}"
        Digest::MD5.hexdigest(key)
      end

      def cache_control
        page.try(:cache_control).presence || site.try(:cache_control).presence || DEFAULT_CACHE_CONTROL
      end

      def cache_vary
        page.try(:cache_vary).presence || site.try(:cache_vary).presence || DEFAULT_CACHE_VARY
      end

      def is_redirect_url?
        return false if page.nil?
        page.try(:redirect_url).blank?
      end

      def marshal(response)
        code, headers, body = response

        # only keep string value headers
        _headers = headers.reject { |key, val| !val.respond_to?(:to_str) }

        Marshal.dump([code, _headers, body])
      end

      def stale?(key)
        env['HTTP_IF_NONE_MATCH'] == key ||
        env['HTTP_IF_MODIFIED_SINCE'] == site.last_modified_at.httpdate
      end

      def cache
        services.cache
      end

    end

  end
end