├── .codeclimate.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── actionpack-page_caching.gemspec ├── gemfiles ├── Gemfile-4-2-stable ├── Gemfile-5-0-stable ├── Gemfile-5-1-stable ├── Gemfile-5-2-stable ├── Gemfile-6-0-stable ├── Gemfile-6-1-stable └── Gemfile-edge ├── lib ├── action_controller │ ├── caching │ │ └── pages.rb │ └── page_caching.rb └── actionpack │ ├── page_caching.rb │ └── page_caching │ └── railtie.rb └── test ├── abstract_unit.rb ├── caching_test.rb └── log_subscriber_test.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | 5 | ratings: 6 | paths: 7 | - "**.rb" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | gemfile: 14 | - '4-2-stable' 15 | - '5-0-stable' 16 | - '5-1-stable' 17 | - '5-2-stable' 18 | - '6-0-stable' 19 | - '6-1-stable' 20 | - 'edge' 21 | ruby: 22 | - '2.4' 23 | - '2.5' 24 | - '2.6' 25 | - '2.7' 26 | - '3.0' 27 | exclude: 28 | - gemfile: '4-2-stable' 29 | ruby: '2.5' 30 | - gemfile: '4-2-stable' 31 | ruby: '2.6' 32 | - gemfile: '4-2-stable' 33 | ruby: '2.7' 34 | - gemfile: '4-2-stable' 35 | ruby: '3.0' 36 | - gemfile: '5-0-stable' 37 | ruby: '2.7' 38 | - gemfile: '5-0-stable' 39 | ruby: '3.0' 40 | - gemfile: '5-1-stable' 41 | ruby: '2.7' 42 | - gemfile: '5-1-stable' 43 | ruby: '3.0' 44 | - gemfile: '5-2-stable' 45 | ruby: '2.7' 46 | - gemfile: '5-2-stable' 47 | ruby: '3.0' 48 | - gemfile: '6-0-stable' 49 | ruby: '2.4' 50 | - gemfile: '6-1-stable' 51 | ruby: '2.4' 52 | - gemfile: 'edge' 53 | ruby: '2.4' 54 | - gemfile: 'edge' 55 | ruby: '2.5' 56 | - gemfile: 'edge' 57 | ruby: '2.6' 58 | fail-fast: false 59 | 60 | runs-on: ubuntu-latest 61 | name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} 62 | 63 | steps: 64 | - uses: actions/checkout@v2 65 | 66 | - uses: ruby/setup-ruby@v1 67 | with: 68 | ruby-version: ${{ matrix.ruby }} 69 | bundler-cache: true 70 | bundler: ${{ fromJSON('["2", "1"]')[matrix.ruby == '2.4'] }} 71 | 72 | - run: bundle exec rake test 73 | 74 | env: 75 | BUNDLE_GEMFILE: gemfiles/Gemfile-${{ matrix.gemfile }} 76 | BUNDLE_JOBS: 4 77 | BUNDLE_RETRY: 3 78 | 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ruby-version 2 | Gemfile.lock 3 | gemfiles/*.lock 4 | pkg/* 5 | test/tmp 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop 4 | # to ignore them, so only the ones explicitly set in this file are enabled. 5 | DisabledByDefault: true 6 | 7 | # Align `when` with `case`. 8 | Layout/CaseIndentation: 9 | Enabled: true 10 | 11 | Layout/ClosingHeredocIndentation: 12 | Enabled: true 13 | 14 | # Align comments with method definitions. 15 | Layout/CommentIndentation: 16 | Enabled: true 17 | 18 | Layout/ElseAlignment: 19 | Enabled: true 20 | 21 | Layout/EmptyLineAfterMagicComment: 22 | Enabled: true 23 | 24 | Layout/EmptyLinesAroundAccessModifier: 25 | Enabled: true 26 | EnforcedStyle: only_before 27 | 28 | Layout/EmptyLinesAroundBlockBody: 29 | Enabled: true 30 | 31 | # In a regular class definition, no empty lines around the body. 32 | Layout/EmptyLinesAroundClassBody: 33 | Enabled: true 34 | 35 | # In a regular method definition, no empty lines around the body. 36 | Layout/EmptyLinesAroundMethodBody: 37 | Enabled: true 38 | 39 | # In a regular module definition, no empty lines around the body. 40 | Layout/EmptyLinesAroundModuleBody: 41 | Enabled: true 42 | 43 | # Align `end` with the matching keyword or starting expression except for 44 | # assignments, where it should be aligned with the LHS. 45 | Layout/EndAlignment: 46 | Enabled: true 47 | EnforcedStyleAlignWith: variable 48 | AutoCorrect: true 49 | 50 | Layout/FirstArgumentIndentation: 51 | Enabled: true 52 | 53 | # Method definitions after `private` or `protected` isolated calls need one 54 | # extra level of indentation. 55 | Layout/IndentationConsistency: 56 | Enabled: true 57 | EnforcedStyle: indented_internal_methods 58 | 59 | # Two spaces, no tabs (for indentation). 60 | Layout/IndentationWidth: 61 | Enabled: true 62 | 63 | Layout/LeadingCommentSpace: 64 | Enabled: true 65 | 66 | Layout/SpaceAfterColon: 67 | Enabled: true 68 | 69 | Layout/SpaceAfterComma: 70 | Enabled: true 71 | 72 | Layout/SpaceAfterSemicolon: 73 | Enabled: true 74 | 75 | Layout/SpaceAroundEqualsInParameterDefault: 76 | Enabled: true 77 | 78 | Layout/SpaceAroundKeyword: 79 | Enabled: true 80 | 81 | # Use `foo {}` not `foo{}`. 82 | Layout/SpaceBeforeBlockBraces: 83 | Enabled: true 84 | 85 | Layout/SpaceBeforeComma: 86 | Enabled: true 87 | 88 | Layout/SpaceBeforeComment: 89 | Enabled: true 90 | 91 | Layout/SpaceBeforeFirstArg: 92 | Enabled: true 93 | 94 | # Use `foo { bar }` not `foo {bar}`. 95 | Layout/SpaceInsideBlockBraces: 96 | Enabled: true 97 | EnforcedStyleForEmptyBraces: space 98 | 99 | # Use `{ a: 1 }` not `{a:1}`. 100 | Layout/SpaceInsideHashLiteralBraces: 101 | Enabled: true 102 | 103 | Layout/SpaceInsideParens: 104 | Enabled: true 105 | 106 | # Detect hard tabs, no hard tabs. 107 | Layout/Tab: 108 | Enabled: true 109 | 110 | # Empty lines should not have any spaces. 111 | Layout/TrailingEmptyLines: 112 | Enabled: true 113 | 114 | # No trailing whitespace. 115 | Layout/TrailingWhitespace: 116 | Enabled: true 117 | 118 | Lint/AmbiguousOperator: 119 | Enabled: true 120 | 121 | Lint/AmbiguousRegexpLiteral: 122 | Enabled: true 123 | 124 | Lint/DeprecatedClassMethods: 125 | Enabled: true 126 | 127 | Lint/ErbNewArguments: 128 | Enabled: true 129 | 130 | Lint/RedundantStringCoercion: 131 | Enabled: true 132 | 133 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 134 | Lint/RequireParentheses: 135 | Enabled: true 136 | 137 | Lint/ShadowingOuterLocalVariable: 138 | Enabled: true 139 | 140 | Lint/UselessAssignment: 141 | Enabled: true 142 | 143 | Lint/UriEscapeUnescape: 144 | Enabled: true 145 | 146 | # Prefer &&/|| over and/or. 147 | Style/AndOr: 148 | Enabled: true 149 | 150 | # Prefer Foo.method over Foo::method 151 | Style/ColonMethodCall: 152 | Enabled: true 153 | 154 | Style/DefWithParentheses: 155 | Enabled: true 156 | 157 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. 158 | Style/HashSyntax: 159 | Enabled: true 160 | 161 | # Defining a method with parameters needs parentheses. 162 | Style/MethodDefParentheses: 163 | Enabled: true 164 | 165 | Style/ParenthesesAroundCondition: 166 | Enabled: true 167 | 168 | Style/RedundantBegin: 169 | Enabled: true 170 | 171 | Style/RedundantFreeze: 172 | Enabled: true 173 | 174 | # Use quotes for string literals when they are enough. 175 | Style/RedundantPercentQ: 176 | Enabled: true 177 | 178 | Style/RedundantReturn: 179 | Enabled: true 180 | AllowMultipleReturnValues: true 181 | 182 | Style/Semicolon: 183 | Enabled: true 184 | AllowAsExpressionSeparator: true 185 | 186 | # Check quotes usage according to lint rule below. 187 | Style/StringLiterals: 188 | Enabled: true 189 | EnforcedStyle: double_quotes 190 | 191 | Style/TrivialAccessors: 192 | Enabled: true 193 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.4 (May 15, 2021) 2 | 3 | - Fix `URI.parser` deprecation warning in Rails 6.1 4 | 5 | _Andrew White_ 6 | 7 | ## 1.2.3 (June 12, 2020) 8 | 9 | - Simplifies code in `page_caching.rb` 10 | 11 | _Xavier Noria_ 12 | 13 | ## 1.2.2 (May 6, 2020) 14 | 15 | - Fix variable name 16 | 17 | _Jack McCracken_ 18 | 19 | ## 1.2.1 (May 6, 2020) 20 | 21 | - Only write relative URIs when their normalized path begins with the normalized cache directory path 22 | 23 | _Jack McCracken_ 24 | 25 | ## 1.2.0 (December 11, 2019) 26 | 27 | - Update RuboCop config 28 | 29 | _Rafael Mendonça França_ 30 | 31 | - Fix for Rails 6 MIME lookups 32 | 33 | _Rob Zolkos_ 34 | 35 | - Remove Rails 4.2 from testing matrix 36 | 37 | _Rob Zolkos_ 38 | 39 | - Minimum of Ruby 2.4 required 40 | 41 | _Rob Zolkos_ 42 | 43 | - Remove upper dependency for `actionpack` 44 | 45 | _Anton Kolodii_ 46 | 47 | ## 1.1.1 (September 25, 2018) 48 | 49 | - Fixes handling of several forward slashes as root path 50 | 51 | _Xavier Noria_ 52 | 53 | - Documentation overhaul 54 | 55 | _Xavier Noria_ 56 | 57 | ## 1.1.0 (January 23, 2017) 58 | 59 | - Support dynamic `page_cache_directory` using a Proc, Symbol or callable 60 | 61 | _Andrew White_ 62 | 63 | - Support instance level setting of `page_cache_directory` 64 | 65 | _Andrew White_ 66 | 67 | - Add support for Rails 5.0 and master 68 | 69 | _Andrew White_ 70 | 71 | ## 1.0.2 (November 15, 2013) 72 | 73 | - Fix load order problem with other gems. 74 | 75 | _Rafael Mendonça França_ 76 | 77 | ## 1.0.1 (October 24, 2013) 78 | 79 | - Add Railtie to set `page_cache_directory` by default to `public` folder. 80 | 81 | Fixes #5. 82 | 83 | _Žiga Vidic_ 84 | 85 | ## 1.0.0 (February 27, 2013) 86 | 87 | - Extract Action Pack - Action Caching from Rails core. 88 | 89 | _Francesco Rodriguez_, _Rafael Mendonça França_, _Michiel Sikkes_ 90 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rails" 6 | gem "rubocop", ">= 0.77.0", require: false 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 David Heinemeier Hansson 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actionpack-page_caching 2 | 3 | Static page caching for Action Pack (removed from core in Rails 4.0). 4 | 5 | ## Introduction 6 | 7 | Page caching is an approach to caching in which response bodies are stored in 8 | files that the web server can serve directly: 9 | 10 | 1. A request to endpoint _E_ arrives. 11 | 2. Its response is calculated and stored in a file _F_. 12 | 3. Next time _E_ is requested, the web server sends _F_ directly. 13 | 14 | That applies only to GET or HEAD requests whose response code is 200, the rest 15 | are ignored. 16 | 17 | Unlike caching proxies or other more sophisticated setups, page caching results 18 | in a dramatic speed up while being dead simple at the same time. Awesome 19 | cost/benefit. 20 | 21 | The reason for such performance boost is that cached endpoints are 22 | short-circuited by the web server, which is very efficient at serving static 23 | files. Requests to cached endpoints do not even reach your Rails application. 24 | 25 | This technique, however, is only suitable for pages that do not need to go 26 | through your Rails stack, precisely. For example, content management systems 27 | like wikis have typically many pages that are a great fit for this approach, but 28 | account-based systems where people log in and manipulate their own data are 29 | often less likely candidates. As a use case you can check, [Rails 30 | Contributors](https://contributors.rubyonrails.org/) makes heavy use of page 31 | caching. Its source code is [here](https://github.com/rails/rails-contributors). 32 | 33 | It is not all or nothing, though, in HTML cached pages JavaScript can still 34 | tweak details here and there dynamically as a trade-off. 35 | 36 | ## Installation 37 | 38 | Add this line to your application's `Gemfile`: 39 | 40 | ``` ruby 41 | gem "actionpack-page_caching" 42 | ``` 43 | 44 | And then execute: 45 | 46 | ``` 47 | $ bundle 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### Enable Caching 53 | 54 | Page caching needs caching enabled: 55 | 56 | ```ruby 57 | config.action_controller.perform_caching = true 58 | ``` 59 | 60 | That goes typically in `config/environments/production.rb`, but you can activate 61 | that flag in any mode. 62 | 63 | Since Rails 5 there is a special toggler to easily enable/disable caching in 64 | `development` mode without editing its configuration file. Just execute 65 | 66 | ``` 67 | $ bin/rails dev:cache 68 | ``` 69 | 70 | to enable/disable caching in `development` mode. 71 | 72 | ### Configure the Cache Directory 73 | 74 | #### Default Cache Directory 75 | 76 | By default, files are stored below the `public` directory of your Rails 77 | application, with a path that matches the one in the URL. 78 | 79 | For example, a page-cached request to `/posts/what-is-new-in-rails-6` would be 80 | stored by default in the file `public/posts/what-is-new-in-rails-6.html`, and 81 | the web server would be configured to check that path in the file system before 82 | falling back to Rails. More on this later. 83 | 84 | #### Custom Cache Directory 85 | 86 | The default page caching directory can be overridden: 87 | 88 | ``` ruby 89 | config.action_controller.page_cache_directory = Rails.root.join("public", "cached_pages") 90 | ``` 91 | 92 | There is no need to ensure the directory exists when the application boots, 93 | whenever a page has to be cached, the page cache directory is created if needed. 94 | 95 | #### Custom Cache Directory per Controller 96 | 97 | The globally configured cache directory, default or custom, can be overridden in 98 | each controller. There are three ways of doing this. 99 | 100 | With a lambda: 101 | 102 | ``` ruby 103 | class WeblogController < ApplicationController 104 | self.page_cache_directory = -> { 105 | Rails.root.join("public", request.domain) 106 | } 107 | end 108 | ``` 109 | 110 | a symbol: 111 | 112 | ``` ruby 113 | class WeblogController < ApplicationController 114 | self.page_cache_directory = :domain_cache_directory 115 | 116 | private 117 | def domain_cache_directory 118 | Rails.root.join("public", request.domain) 119 | end 120 | end 121 | ``` 122 | 123 | or a callable object: 124 | 125 | ``` ruby 126 | class DomainCacheDirectory 127 | def self.call(request) 128 | Rails.root.join("public", request.domain) 129 | end 130 | end 131 | 132 | class WeblogController < ApplicationController 133 | self.page_cache_directory = DomainCacheDirectory 134 | end 135 | ``` 136 | 137 | Intermediate directories are created as needed also in this case. 138 | 139 | ### Specify Actions to be Cached 140 | 141 | Specifying which actions have to be cached is done through the `caches_page` class method: 142 | 143 | ``` ruby 144 | class WeblogController < ActionController::Base 145 | caches_page :show, :new 146 | end 147 | ``` 148 | 149 | ### Configure The Web Server 150 | 151 | The [wiki](https://github.com/rails/actionpack-page_caching/wiki) of the project 152 | has some examples of web server configuration. 153 | 154 | ### Cache Expiration 155 | 156 | Expiration of the cache is handled by deleting the cached files, which results 157 | in a lazy regeneration approach in which the content is stored again as cached 158 | endpoints are hit. 159 | 160 | #### Full Cache Expiration 161 | 162 | If the cache is stored in a separate directory like `public/cached_pages`, you 163 | can easily expire the whole thing by removing said directory. 164 | 165 | Removing a directory recursively with something like `rm -rf` is unreliable 166 | because that operation is not atomic and can mess up with concurrent page cache 167 | generation. 168 | 169 | In POSIX systems moving a file is atomic, so the recommended approach would be 170 | to move the directory first out of the way, and then recursively delete that 171 | one. Something like 172 | 173 | ```bash 174 | #!/bin/bash 175 | 176 | tmp=public/cached_pages-$(date +%s) 177 | mv public/cached_pages $tmp 178 | rm -rf $tmp 179 | ``` 180 | 181 | As noted before, the page cache directory is created if it does not exist, so 182 | moving the directory is enough to have a clean cache, no need to recreate. 183 | 184 | #### Fine-grained Cache Expiration 185 | 186 | The API for doing so mimics the options from `url_for` and friends: 187 | 188 | ``` ruby 189 | class WeblogController < ActionController::Base 190 | def update 191 | List.update(params[:list][:id], params[:list]) 192 | expire_page action: "show", id: params[:list][:id] 193 | redirect_to action: "show", id: params[:list][:id] 194 | end 195 | end 196 | ``` 197 | 198 | Additionally, you can expire caches using 199 | [Sweepers](https://github.com/rails/rails-observers#action-controller-sweeper) 200 | that act on changes in the model to determine when a cache is supposed to be 201 | expired. 202 | 203 | Contributing 204 | ------------ 205 | 206 | 1. Fork it. 207 | 2. Create your feature branch (`git checkout -b my-new-feature`). 208 | 3. Commit your changes (`git commit -am 'Add some feature'`). 209 | 4. Push to the branch (`git push origin my-new-feature`). 210 | 5. Create a new Pull Request. 211 | 212 | Code Status 213 | ----------- 214 | 215 | * [![Build Status](https://github.com/rails/actionpack-page_caching/actions/workflows/ci.yml/badge.svg)](https://github.com/rails/actionpack-page_caching/actions/workflows/ci.yml) 216 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "rake/testtask" 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs = ["test"] 7 | t.pattern = "test/**/*_test.rb" 8 | t.ruby_opts = ["-w"] 9 | end 10 | 11 | task default: :test 12 | -------------------------------------------------------------------------------- /actionpack-page_caching.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |gem| 2 | gem.name = "actionpack-page_caching" 3 | gem.version = "1.2.4" 4 | gem.author = "David Heinemeier Hansson" 5 | gem.email = "david@loudthinking.com" 6 | gem.description = "Static page caching for Action Pack (removed from core in Rails 4.0)" 7 | gem.summary = "Static page caching for Action Pack (removed from core in Rails 4.0)" 8 | gem.homepage = "https://github.com/rails/actionpack-page_caching" 9 | gem.license = "MIT" 10 | 11 | gem.required_ruby_version = ">= 1.9.3" 12 | gem.files = `git ls-files`.split($/) 13 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 14 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 15 | gem.require_paths = ["lib"] 16 | gem.license = "MIT" 17 | 18 | gem.add_dependency "actionpack", ">= 4.0.0" 19 | 20 | gem.add_development_dependency "mocha" 21 | end 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-4-2-stable: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "4-2-stable" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-5-0-stable: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "5-0-stable" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-5-1-stable: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "5-1-stable" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-5-2-stable: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "5-2-stable" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-6-0-stable: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "6-0-stable" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-6-1-stable: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "6-1-stable" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-edge: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rails", github: "rails/rails", branch: "main" 6 | -------------------------------------------------------------------------------- /lib/action_controller/caching/pages.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "uri" 3 | require "active_support/core_ext/class/attribute_accessors" 4 | require "active_support/core_ext/string/strip" 5 | 6 | module ActionController 7 | module Caching 8 | # Page caching is an approach to caching where the entire action output of is 9 | # stored as a HTML file that the web server can serve without going through 10 | # Action Pack. This is the fastest way to cache your content as opposed to going 11 | # dynamically through the process of generating the content. Unfortunately, this 12 | # incredible speed-up is only available to stateless pages where all visitors are 13 | # treated the same. Content management systems -- including weblogs and wikis -- 14 | # have many pages that are a great fit for this approach, but account-based systems 15 | # where people log in and manipulate their own data are often less likely candidates. 16 | # 17 | # Specifying which actions to cache is done through the +caches_page+ class method: 18 | # 19 | # class WeblogController < ActionController::Base 20 | # caches_page :show, :new 21 | # end 22 | # 23 | # This will generate cache files such as weblog/show/5.html and 24 | # weblog/new.html, which match the URLs used that would normally trigger 25 | # dynamic page generation. Page caching works by configuring a web server to first 26 | # check for the existence of files on disk, and to serve them directly when found, 27 | # without passing the request through to Action Pack. This is much faster than 28 | # handling the full dynamic request in the usual way. 29 | # 30 | # Expiration of the cache is handled by deleting the cached file, which results 31 | # in a lazy regeneration approach where the cache is not restored before another 32 | # hit is made against it. The API for doing so mimics the options from +url_for+ and friends: 33 | # 34 | # class WeblogController < ActionController::Base 35 | # def update 36 | # List.update(params[:list][:id], params[:list]) 37 | # expire_page action: "show", id: params[:list][:id] 38 | # redirect_to action: "show", id: params[:list][:id] 39 | # end 40 | # end 41 | # 42 | # Additionally, you can expire caches using Sweepers that act on changes in 43 | # the model to determine when a cache is supposed to be expired. 44 | module Pages 45 | extend ActiveSupport::Concern 46 | 47 | included do 48 | # The cache directory should be the document root for the web server and is 49 | # set using Base.page_cache_directory = "/document/root". For Rails, 50 | # this directory has already been set to Rails.public_path (which is usually 51 | # set to Rails.root + "/public"). Changing this setting can be useful 52 | # to avoid naming conflicts with files in public/, but doing so will 53 | # likely require configuring your web server to look in the new location for 54 | # cached files. 55 | class_attribute :page_cache_directory 56 | self.page_cache_directory ||= "" 57 | 58 | # The compression used for gzip. If +false+ (default), the page is not compressed. 59 | # If can be a symbol showing the ZLib compression method, for example, :best_compression 60 | # or :best_speed or an integer configuring the compression level. 61 | class_attribute :page_cache_compression 62 | self.page_cache_compression ||= false 63 | end 64 | 65 | class PageCache #:nodoc: 66 | def initialize(cache_directory, default_extension, controller = nil) 67 | @cache_directory = cache_directory 68 | @default_extension = default_extension 69 | @controller = controller 70 | end 71 | 72 | def expire(path) 73 | instrument :expire_page, path do 74 | delete(cache_path(path)) 75 | end 76 | end 77 | 78 | def cache(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION) 79 | instrument :write_page, path do 80 | write(content, cache_path(path, extension), gzip) 81 | end 82 | end 83 | 84 | private 85 | def cache_directory 86 | case @cache_directory 87 | when Proc 88 | handle_proc_cache_directory 89 | when Symbol 90 | handle_symbol_cache_directory 91 | else 92 | handle_default_cache_directory 93 | end 94 | end 95 | 96 | def normalized_cache_directory 97 | File.expand_path(cache_directory) 98 | end 99 | 100 | def handle_proc_cache_directory 101 | if @controller 102 | @controller.instance_exec(&@cache_directory) 103 | else 104 | raise_runtime_error 105 | end 106 | end 107 | 108 | def handle_symbol_cache_directory 109 | if @controller 110 | @controller.send(@cache_directory) 111 | else 112 | raise_runtime_error 113 | end 114 | end 115 | 116 | def handle_callable_cache_directory 117 | if @controller 118 | @cache_directory.call(@controller.request) 119 | else 120 | raise_runtime_error 121 | end 122 | end 123 | 124 | def handle_default_cache_directory 125 | if @cache_directory.respond_to?(:call) 126 | handle_callable_cache_directory 127 | else 128 | @cache_directory.to_s 129 | end 130 | end 131 | 132 | def raise_runtime_error 133 | raise RuntimeError, <<-MSG.strip_heredoc 134 | Dynamic page_cache_directory used with class-level cache_page method 135 | 136 | You have specified either a Proc, Symbol or callable object for page_cache_directory 137 | which needs to be executed within the context of a request. If you need to call the 138 | cache_page method from a class-level context then set the page_cache_directory to a 139 | static value and override the setting at the instance-level using before_action. 140 | MSG 141 | end 142 | 143 | attr_reader :default_extension 144 | 145 | def cache_file(path, extension) 146 | if path.empty? || path =~ %r{\A/+\z} 147 | name = "/index" 148 | else 149 | name = URI::DEFAULT_PARSER.unescape(path.chomp("/")) 150 | end 151 | 152 | if File.extname(name).empty? 153 | name + (extension || default_extension) 154 | else 155 | name 156 | end 157 | end 158 | 159 | def cache_path(path, extension = nil) 160 | unnormalized_path = File.join(normalized_cache_directory, cache_file(path, extension)) 161 | normalized_path = File.expand_path(unnormalized_path) 162 | 163 | normalized_path if normalized_path.start_with?(normalized_cache_directory) 164 | end 165 | 166 | def delete(path) 167 | return unless path 168 | 169 | File.delete(path) if File.exist?(path) 170 | File.delete(path + ".gz") if File.exist?(path + ".gz") 171 | end 172 | 173 | def write(content, path, gzip) 174 | return unless path 175 | 176 | FileUtils.makedirs(File.dirname(path)) 177 | File.open(path, "wb+") { |f| f.write(content) } 178 | 179 | if gzip 180 | Zlib::GzipWriter.open(path + ".gz", gzip) { |f| f.write(content) } 181 | end 182 | end 183 | 184 | def instrument(name, path) 185 | ActiveSupport::Notifications.instrument("#{name}.action_controller", path: path) { yield } 186 | end 187 | end 188 | 189 | module ClassMethods 190 | # Expires the page that was cached with the +path+ as a key. 191 | # 192 | # expire_page "/lists/show" 193 | def expire_page(path) 194 | if perform_caching 195 | page_cache.expire(path) 196 | end 197 | end 198 | 199 | # Manually cache the +content+ in the key determined by +path+. 200 | # 201 | # cache_page "I'm the cached content", "/lists/show" 202 | def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION) 203 | if perform_caching 204 | page_cache.cache(content, path, extension, gzip) 205 | end 206 | end 207 | 208 | # Caches the +actions+ using the page-caching approach that'll store 209 | # the cache in a path within the +page_cache_directory+ that 210 | # matches the triggering url. 211 | # 212 | # You can also pass a :gzip option to override the class configuration one. 213 | # 214 | # # cache the index action 215 | # caches_page :index 216 | # 217 | # # cache the index action except for JSON requests 218 | # caches_page :index, if: Proc.new { !request.format.json? } 219 | # 220 | # # don't gzip images 221 | # caches_page :image, gzip: false 222 | def caches_page(*actions) 223 | if perform_caching 224 | options = actions.extract_options! 225 | 226 | gzip_level = options.fetch(:gzip, page_cache_compression) 227 | gzip_level = \ 228 | case gzip_level 229 | when Symbol 230 | Zlib.const_get(gzip_level.upcase) 231 | when Integer 232 | gzip_level 233 | when false 234 | nil 235 | else 236 | Zlib::BEST_COMPRESSION 237 | end 238 | 239 | after_action({ only: actions }.merge(options)) do |c| 240 | c.cache_page(nil, nil, gzip_level) 241 | end 242 | end 243 | end 244 | 245 | private 246 | def page_cache 247 | PageCache.new(page_cache_directory, default_static_extension) 248 | end 249 | end 250 | 251 | # Expires the page that was cached with the +options+ as a key. 252 | # 253 | # expire_page controller: "lists", action: "show" 254 | def expire_page(options = {}) 255 | if perform_caching? 256 | case options 257 | when Hash 258 | case options[:action] 259 | when Array 260 | options[:action].each { |action| expire_page(options.merge(action: action)) } 261 | else 262 | page_cache.expire(url_for(options.merge(only_path: true))) 263 | end 264 | else 265 | page_cache.expire(options) 266 | end 267 | end 268 | end 269 | 270 | # Manually cache the +content+ in the key determined by +options+. If no content is provided, 271 | # the contents of response.body is used. If no options are provided, the url of the current 272 | # request being handled is used. 273 | # 274 | # cache_page "I'm the cached content", controller: "lists", action: "show" 275 | def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION) 276 | if perform_caching? && caching_allowed? 277 | path = \ 278 | case options 279 | when Hash 280 | url_for(options.merge(only_path: true, format: params[:format])) 281 | when String 282 | options 283 | else 284 | request.path 285 | end 286 | 287 | type = if self.respond_to?(:media_type) 288 | Mime::LOOKUP[self.media_type] 289 | else 290 | Mime::LOOKUP[self.content_type] 291 | end 292 | 293 | if type && (type_symbol = type.symbol).present? 294 | extension = ".#{type_symbol}" 295 | end 296 | 297 | page_cache.cache(content || response.body, path, extension, gzip) 298 | end 299 | end 300 | 301 | def caching_allowed? 302 | (request.get? || request.head?) && response.status == 200 303 | end 304 | 305 | def perform_caching? 306 | self.class.perform_caching 307 | end 308 | 309 | private 310 | def page_cache 311 | PageCache.new(page_cache_directory, default_static_extension, self) 312 | end 313 | end 314 | end 315 | end 316 | -------------------------------------------------------------------------------- /lib/action_controller/page_caching.rb: -------------------------------------------------------------------------------- 1 | require "action_controller/caching/pages" 2 | 3 | module ActionController 4 | module Caching 5 | include Pages 6 | end 7 | end 8 | 9 | ActionController::Base.include(ActionController::Caching::Pages) 10 | -------------------------------------------------------------------------------- /lib/actionpack/page_caching.rb: -------------------------------------------------------------------------------- 1 | require "actionpack/page_caching/railtie" 2 | -------------------------------------------------------------------------------- /lib/actionpack/page_caching/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails/railtie" 2 | 3 | module ActionPack 4 | module PageCaching 5 | class Railtie < Rails::Railtie 6 | initializer "action_pack.page_caching" do 7 | ActiveSupport.on_load(:action_controller) do 8 | require "action_controller/page_caching" 9 | end 10 | end 11 | 12 | initializer "action_pack.page_caching.set_config", before: "action_controller.set_configs" do |app| 13 | app.config.action_controller.page_cache_directory ||= app.config.paths["public"].first 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/abstract_unit.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "minitest/autorun" 3 | require "action_controller" 4 | require "action_controller/page_caching" 5 | 6 | if ActiveSupport.respond_to?(:test_order) 7 | ActiveSupport.test_order = :random 8 | end 9 | 10 | if ActionController::Base.respond_to?(:enable_fragment_cache_logging=) 11 | ActionController::Base.enable_fragment_cache_logging = true 12 | end 13 | -------------------------------------------------------------------------------- /test/caching_test.rb: -------------------------------------------------------------------------------- 1 | require "rails/version" 2 | require "abstract_unit" 3 | require "mocha/minitest" 4 | require "find" 5 | 6 | CACHE_DIR = "test_cache" 7 | # Don't change "../tmp" cavalierly or you might hose something you don't want hosed 8 | TEST_TMP_DIR = File.expand_path("../tmp", __FILE__) 9 | FILE_STORE_PATH = File.join(TEST_TMP_DIR, CACHE_DIR) 10 | 11 | 12 | module PageCachingTestHelpers 13 | def setup 14 | super 15 | 16 | @routes = ActionDispatch::Routing::RouteSet.new 17 | 18 | FileUtils.rm_rf(File.dirname(FILE_STORE_PATH)) 19 | FileUtils.mkdir_p(FILE_STORE_PATH) 20 | end 21 | 22 | def teardown 23 | super 24 | 25 | FileUtils.rm_rf(File.dirname(FILE_STORE_PATH)) 26 | @controller.perform_caching = false 27 | end 28 | 29 | private 30 | def assert_page_cached(action, options = {}) 31 | expected = options[:content] || action.to_s 32 | path = cache_file(action, options) 33 | 34 | assert File.exist?(path), "The cache file #{path} doesn't exist" 35 | 36 | if File.extname(path) == ".gz" 37 | actual = Zlib::GzipReader.open(path) { |f| f.read } 38 | else 39 | actual = File.read(path) 40 | end 41 | 42 | assert_equal expected, actual, "The cached content doesn't match the expected value" 43 | end 44 | 45 | def assert_page_not_cached(action, options = {}) 46 | path = cache_file(action, options) 47 | assert !File.exist?(path), "The cache file #{path} still exists" 48 | end 49 | 50 | def cache_file(action, options = {}) 51 | path = options[:path] || FILE_STORE_PATH 52 | controller = options[:controller] || self.class.name.underscore 53 | format = options[:format] || "html" 54 | 55 | "#{path}/#{controller}/#{action}.#{format}" 56 | end 57 | 58 | def draw(&block) 59 | @routes = ActionDispatch::Routing::RouteSet.new 60 | @routes.draw(&block) 61 | @controller.extend(@routes.url_helpers) 62 | end 63 | end 64 | 65 | class CachingMetalController < ActionController::Metal 66 | abstract! 67 | 68 | include AbstractController::Callbacks 69 | include ActionController::Caching 70 | 71 | self.page_cache_directory = FILE_STORE_PATH 72 | self.cache_store = :file_store, FILE_STORE_PATH 73 | end 74 | 75 | class PageCachingMetalTestController < CachingMetalController 76 | caches_page :ok 77 | 78 | def ok 79 | self.response_body = "ok" 80 | end 81 | end 82 | 83 | class PageCachingMetalTest < ActionController::TestCase 84 | include PageCachingTestHelpers 85 | tests PageCachingMetalTestController 86 | 87 | def test_should_cache_get_with_ok_status 88 | draw do 89 | get "/page_caching_metal_test/ok", to: "page_caching_metal_test#ok" 90 | end 91 | 92 | get :ok 93 | assert_response :ok 94 | assert_page_cached :ok 95 | end 96 | end 97 | 98 | ActionController::Base.page_cache_directory = FILE_STORE_PATH 99 | 100 | class CachingController < ActionController::Base 101 | abstract! 102 | 103 | self.cache_store = :file_store, FILE_STORE_PATH 104 | 105 | protected 106 | if ActionPack::VERSION::STRING < "4.1" 107 | def render(options) 108 | if options.key?(:html) 109 | super({ text: options.delete(:html) }.merge(options)) 110 | else 111 | super 112 | end 113 | end 114 | end 115 | end 116 | 117 | class PageCachingTestController < CachingController 118 | self.page_cache_compression = :best_compression 119 | 120 | caches_page :ok, :no_content, if: Proc.new { |c| !c.request.format.json? } 121 | caches_page :found, :not_found 122 | caches_page :about_me 123 | caches_page :default_gzip 124 | caches_page :no_gzip, gzip: false 125 | caches_page :gzip_level, gzip: :best_speed 126 | 127 | def ok 128 | render html: "ok" 129 | end 130 | 131 | def no_content 132 | head :no_content 133 | end 134 | 135 | def found 136 | redirect_to action: "ok" 137 | end 138 | 139 | def not_found 140 | head :not_found 141 | end 142 | 143 | def custom_path 144 | render html: "custom_path" 145 | cache_page(nil, "/index.html") 146 | end 147 | 148 | def default_gzip 149 | render html: "default_gzip" 150 | end 151 | 152 | def no_gzip 153 | render html: "no_gzip" 154 | end 155 | 156 | def gzip_level 157 | render html: "gzip_level" 158 | end 159 | 160 | def expire_custom_path 161 | expire_page("/index.html") 162 | head :ok 163 | end 164 | 165 | def trailing_slash 166 | render html: "trailing_slash" 167 | end 168 | 169 | def about_me 170 | respond_to do |format| 171 | format.html { render html: "I am html" } 172 | format.xml { render xml: "I am xml" } 173 | end 174 | end 175 | end 176 | 177 | class PageCachingTest < ActionController::TestCase 178 | include PageCachingTestHelpers 179 | tests PageCachingTestController 180 | 181 | def test_cache_does_not_escape 182 | draw do 183 | get "/page_caching_test/ok/:id", to: "page_caching_test#ok" 184 | end 185 | 186 | project_root = File.expand_path("../../", __FILE__) 187 | 188 | # Make a path that escapes the cache directory 189 | get_to_root = "../../../" 190 | 191 | # Make sure this relative path points at the project root 192 | assert_equal project_root, File.expand_path(File.join(FILE_STORE_PATH, get_to_root)) 193 | 194 | if Rails.version =~ /^4\./ 195 | get :ok, id: "#{get_to_root}../pwnd" 196 | else 197 | get :ok, params: { id: "#{get_to_root}../pwnd" } 198 | end 199 | 200 | assert_predicate Find.find(File.join(project_root, "test")).grep(/pwnd/), :empty? 201 | end 202 | 203 | def test_page_caching_resources_saves_to_correct_path_with_extension_even_if_default_route 204 | draw do 205 | get "posts.:format", to: "posts#index", as: :formatted_posts 206 | get "/", to: "posts#index", as: :main 207 | end 208 | 209 | defaults = { controller: "posts", action: "index", only_path: true } 210 | 211 | assert_equal "/posts.rss", @routes.url_for(defaults.merge(format: "rss")) 212 | assert_equal "/", @routes.url_for(defaults.merge(format: nil)) 213 | end 214 | 215 | def test_should_cache_head_with_ok_status 216 | draw do 217 | get "/page_caching_test/ok", to: "page_caching_test#ok" 218 | end 219 | 220 | head :ok 221 | assert_response :ok 222 | assert_page_cached :ok 223 | end 224 | 225 | def test_should_cache_get_with_ok_status 226 | draw do 227 | get "/page_caching_test/ok", to: "page_caching_test#ok" 228 | end 229 | 230 | get :ok 231 | assert_response :ok 232 | assert_page_cached :ok 233 | end 234 | 235 | def test_should_cache_with_custom_path 236 | draw do 237 | get "/page_caching_test/custom_path", to: "page_caching_test#custom_path" 238 | end 239 | 240 | get :custom_path 241 | assert_page_cached :index, controller: ".", content: "custom_path" 242 | end 243 | 244 | def test_should_expire_cache_with_custom_path 245 | draw do 246 | get "/page_caching_test/custom_path", to: "page_caching_test#custom_path" 247 | get "/page_caching_test/expire_custom_path", to: "page_caching_test#expire_custom_path" 248 | end 249 | 250 | get :custom_path 251 | assert_page_cached :index, controller: ".", content: "custom_path" 252 | 253 | get :expire_custom_path 254 | assert_page_not_cached :index, controller: ".", content: "custom_path" 255 | end 256 | 257 | def test_should_gzip_cache 258 | draw do 259 | get "/page_caching_test/custom_path", to: "page_caching_test#custom_path" 260 | get "/page_caching_test/expire_custom_path", to: "page_caching_test#expire_custom_path" 261 | end 262 | 263 | get :custom_path 264 | assert_page_cached :index, controller: ".", format: "html.gz", content: "custom_path" 265 | 266 | get :expire_custom_path 267 | assert_page_not_cached :index, controller: ".", format: "html.gz" 268 | end 269 | 270 | def test_should_allow_to_disable_gzip 271 | draw do 272 | get "/page_caching_test/no_gzip", to: "page_caching_test#no_gzip" 273 | end 274 | 275 | get :no_gzip 276 | assert_page_cached :no_gzip, format: "html" 277 | assert_page_not_cached :no_gzip, format: "html.gz" 278 | end 279 | 280 | def test_should_use_config_gzip_by_default 281 | draw do 282 | get "/page_caching_test/default_gzip", to: "page_caching_test#default_gzip" 283 | end 284 | 285 | @controller.expects(:cache_page).with(nil, nil, Zlib::BEST_COMPRESSION) 286 | get :default_gzip 287 | end 288 | 289 | def test_should_set_gzip_level 290 | draw do 291 | get "/page_caching_test/gzip_level", to: "page_caching_test#gzip_level" 292 | end 293 | 294 | @controller.expects(:cache_page).with(nil, nil, Zlib::BEST_SPEED) 295 | get :gzip_level 296 | end 297 | 298 | def test_should_cache_without_trailing_slash_on_url 299 | @controller.class.cache_page "cached content", "/page_caching_test/trailing_slash" 300 | assert_page_cached :trailing_slash, content: "cached content" 301 | end 302 | 303 | def test_should_obey_http_accept_attribute 304 | draw do 305 | get "/page_caching_test/about_me", to: "page_caching_test#about_me" 306 | end 307 | 308 | @request.env["HTTP_ACCEPT"] = "text/xml" 309 | get :about_me 310 | assert_equal "I am xml", @response.body 311 | assert_page_cached :about_me, format: "xml", content: "I am xml" 312 | end 313 | 314 | def test_cached_page_should_not_have_trailing_slash_even_if_url_has_trailing_slash 315 | @controller.class.cache_page "cached content", "/page_caching_test/trailing_slash/" 316 | assert_page_cached :trailing_slash, content: "cached content" 317 | end 318 | 319 | def test_should_cache_ok_at_custom_path 320 | draw do 321 | get "/page_caching_test/ok", to: "page_caching_test#ok" 322 | end 323 | 324 | @request.env["PATH_INFO"] = "/index.html" 325 | get :ok 326 | assert_response :ok 327 | assert_page_cached :index, controller: ".", content: "ok" 328 | end 329 | 330 | [:ok, :no_content, :found, :not_found].each do |status| 331 | [:get, :post, :patch, :put, :delete].each do |method| 332 | unless method == :get && status == :ok 333 | define_method "test_shouldnt_cache_#{method}_with_#{status}_status" do 334 | draw do 335 | get "/page_caching_test/ok", to: "page_caching_test#ok" 336 | match "/page_caching_test/#{status}", to: "page_caching_test##{status}", via: method 337 | end 338 | 339 | send(method, status) 340 | assert_response status 341 | assert_page_not_cached status 342 | end 343 | end 344 | end 345 | end 346 | 347 | def test_page_caching_conditional_options 348 | draw do 349 | get "/page_caching_test/ok", to: "page_caching_test#ok" 350 | end 351 | 352 | get :ok, format: "json" 353 | assert_page_not_cached :ok 354 | end 355 | 356 | def test_page_caching_directory_set_as_pathname 357 | ActionController::Base.page_cache_directory = Pathname.new(FILE_STORE_PATH) 358 | 359 | draw do 360 | get "/page_caching_test/ok", to: "page_caching_test#ok" 361 | end 362 | 363 | get :ok 364 | assert_response :ok 365 | assert_page_cached :ok 366 | ensure 367 | ActionController::Base.page_cache_directory = FILE_STORE_PATH 368 | end 369 | 370 | def test_page_caching_directory_set_on_controller_instance 371 | draw do 372 | get "/page_caching_test/ok", to: "page_caching_test#ok" 373 | end 374 | 375 | file_store_path = File.join(TEST_TMP_DIR, "instance_cache") 376 | @controller.page_cache_directory = file_store_path 377 | 378 | get :ok 379 | assert_response :ok 380 | assert_page_cached :ok, path: file_store_path 381 | end 382 | end 383 | 384 | class ProcPageCachingTestController < CachingController 385 | self.page_cache_directory = -> { File.join(TEST_TMP_DIR, request.domain) } 386 | 387 | caches_page :ok 388 | 389 | def ok 390 | render html: "ok" 391 | end 392 | 393 | def expire_ok 394 | expire_page action: :ok 395 | head :ok 396 | end 397 | end 398 | 399 | class ProcPageCachingTest < ActionController::TestCase 400 | include PageCachingTestHelpers 401 | tests ProcPageCachingTestController 402 | 403 | def test_page_is_cached_by_domain 404 | draw do 405 | get "/proc_page_caching_test/ok", to: "proc_page_caching_test#ok" 406 | get "/proc_page_caching_test/ok/expire", to: "proc_page_caching_test#expire_ok" 407 | end 408 | 409 | @request.env["HTTP_HOST"] = "www.foo.com" 410 | get :ok 411 | assert_response :ok 412 | assert_page_cached :ok, path: TEST_TMP_DIR + "/foo.com" 413 | 414 | get :expire_ok 415 | assert_response :ok 416 | assert_page_not_cached :ok, path: TEST_TMP_DIR + "/foo.com" 417 | 418 | @request.env["HTTP_HOST"] = "www.bar.com" 419 | get :ok 420 | assert_response :ok 421 | assert_page_cached :ok, path: TEST_TMP_DIR + "/bar.com" 422 | 423 | get :expire_ok 424 | assert_response :ok 425 | assert_page_not_cached :ok, path: TEST_TMP_DIR + "/bar.com" 426 | end 427 | 428 | def test_class_level_cache_page_raise_error 429 | assert_raises(RuntimeError, /class-level cache_page method/) do 430 | @controller.class.cache_page "cached content", "/proc_page_caching_test/ok" 431 | end 432 | end 433 | end 434 | 435 | class SymbolPageCachingTestController < CachingController 436 | self.page_cache_directory = :domain_cache_directory 437 | 438 | caches_page :ok 439 | 440 | def ok 441 | render html: "ok" 442 | end 443 | 444 | def expire_ok 445 | expire_page action: :ok 446 | head :ok 447 | end 448 | 449 | protected 450 | def domain_cache_directory 451 | File.join(TEST_TMP_DIR, request.domain) 452 | end 453 | end 454 | 455 | class SymbolPageCachingTest < ActionController::TestCase 456 | include PageCachingTestHelpers 457 | tests SymbolPageCachingTestController 458 | 459 | def test_page_is_cached_by_domain 460 | draw do 461 | get "/symbol_page_caching_test/ok", to: "symbol_page_caching_test#ok" 462 | get "/symbol_page_caching_test/ok/expire", to: "symbol_page_caching_test#expire_ok" 463 | end 464 | 465 | @request.env["HTTP_HOST"] = "www.foo.com" 466 | get :ok 467 | assert_response :ok 468 | assert_page_cached :ok, path: TEST_TMP_DIR + "/foo.com" 469 | 470 | get :expire_ok 471 | assert_response :ok 472 | assert_page_not_cached :ok, path: TEST_TMP_DIR + "/foo.com" 473 | 474 | @request.env["HTTP_HOST"] = "www.bar.com" 475 | get :ok 476 | assert_response :ok 477 | assert_page_cached :ok, path: TEST_TMP_DIR + "/bar.com" 478 | 479 | get :expire_ok 480 | assert_response :ok 481 | assert_page_not_cached :ok, path: TEST_TMP_DIR + "/bar.com" 482 | end 483 | 484 | def test_class_level_cache_page_raise_error 485 | assert_raises(RuntimeError, /class-level cache_page method/) do 486 | @controller.class.cache_page "cached content", "/symbol_page_caching_test/ok" 487 | end 488 | end 489 | end 490 | 491 | class CallablePageCachingTestController < CachingController 492 | class DomainCacheDirectory 493 | def self.call(request) 494 | File.join(TEST_TMP_DIR, request.domain) 495 | end 496 | end 497 | 498 | self.page_cache_directory = DomainCacheDirectory 499 | 500 | caches_page :ok 501 | 502 | def ok 503 | render html: "ok" 504 | end 505 | 506 | def expire_ok 507 | expire_page action: :ok 508 | head :ok 509 | end 510 | end 511 | 512 | class CallablePageCachingTest < ActionController::TestCase 513 | include PageCachingTestHelpers 514 | tests CallablePageCachingTestController 515 | 516 | def test_page_is_cached_by_domain 517 | draw do 518 | get "/callable_page_caching_test/ok", to: "callable_page_caching_test#ok" 519 | get "/callable_page_caching_test/ok/expire", to: "callable_page_caching_test#expire_ok" 520 | end 521 | 522 | @request.env["HTTP_HOST"] = "www.foo.com" 523 | get :ok 524 | assert_response :ok 525 | assert_page_cached :ok, path: TEST_TMP_DIR + "/foo.com" 526 | 527 | get :expire_ok 528 | assert_response :ok 529 | assert_page_not_cached :ok, path: TEST_TMP_DIR + "/foo.com" 530 | 531 | @request.env["HTTP_HOST"] = "www.bar.com" 532 | get :ok 533 | assert_response :ok 534 | assert_page_cached :ok, path: TEST_TMP_DIR + "/bar.com" 535 | 536 | get :expire_ok 537 | assert_response :ok 538 | assert_page_not_cached :ok, path: TEST_TMP_DIR + "/bar.com" 539 | end 540 | 541 | def test_class_level_cache_page_raise_error 542 | assert_raises(RuntimeError, /class-level cache_page method/) do 543 | @controller.class.cache_page "cached content", "/callable_page_caching_test/ok" 544 | end 545 | end 546 | end 547 | -------------------------------------------------------------------------------- /test/log_subscriber_test.rb: -------------------------------------------------------------------------------- 1 | require "abstract_unit" 2 | require "active_support/log_subscriber/test_helper" 3 | require "action_controller/log_subscriber" 4 | 5 | module Another 6 | class LogSubscribersController < ActionController::Base 7 | abstract! 8 | 9 | self.perform_caching = true 10 | 11 | def with_page_cache 12 | cache_page("Super soaker", "/index.html") 13 | head :ok 14 | end 15 | end 16 | end 17 | 18 | class ACLogSubscriberTest < ActionController::TestCase 19 | tests Another::LogSubscribersController 20 | include ActiveSupport::LogSubscriber::TestHelper 21 | 22 | def setup 23 | super 24 | 25 | @routes = ActionDispatch::Routing::RouteSet.new 26 | 27 | @cache_path = File.expand_path("../tmp/test_cache", __FILE__) 28 | ActionController::Base.page_cache_directory = @cache_path 29 | @controller.cache_store = :file_store, @cache_path 30 | ActionController::LogSubscriber.attach_to :action_controller 31 | end 32 | 33 | def teardown 34 | ActiveSupport::LogSubscriber.log_subscribers.clear 35 | FileUtils.rm_rf(@cache_path) 36 | end 37 | 38 | def set_logger(logger) 39 | ActionController::Base.logger = logger 40 | end 41 | 42 | def test_with_page_cache 43 | with_routing do |set| 44 | set.draw do 45 | get "/with_page_cache", to: "another/log_subscribers#with_page_cache" 46 | end 47 | 48 | get :with_page_cache 49 | wait 50 | 51 | logs = @logger.logged(:info) 52 | assert_equal 3, logs.size 53 | assert_match(/Write page/, logs[1]) 54 | assert_match(/\/index\.html/, logs[1]) 55 | end 56 | end 57 | end 58 | --------------------------------------------------------------------------------