├── .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 | * [](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 |
--------------------------------------------------------------------------------