1+ # frozen_string_literal: true
2+
13require "fileutils"
24require "uri"
35require "active_support/core_ext/class/attribute_accessors"
46require "active_support/core_ext/string/strip"
57
68module ActionController
79 module Caching
10+ COMPRESSIONS_DEFAULTS = { gzip : 9 , brotli : 9 } # ::Zlib::BEST_COMPRESSION
11+
812 # Page caching is an approach to caching where the entire action output of is
913 # stored as a HTML file that the web server can serve without going through
1014 # Action Pack. This is the fastest way to cache your content as opposed to going
@@ -60,6 +64,9 @@ module Pages
6064 # or <tt>:best_speed</tt> or an integer configuring the compression level.
6165 class_attribute :page_cache_compression
6266 self . page_cache_compression ||= false
67+
68+ class_attribute :page_cache_compressions
69+ self . page_cache_compressions ||= COMPRESSIONS_DEFAULTS
6370 end
6471
6572 class PageCache #:nodoc:
@@ -75,9 +82,9 @@ def expire(path)
7582 end
7683 end
7784
78- def cache ( content , path , extension = nil , gzip = Zlib :: BEST_COMPRESSION )
85+ def cache ( content , path , extension = nil , compressions : COMPRESSIONS_DEFAULTS )
7986 instrument :write_page , path do
80- write ( content , cache_path ( path , extension ) , gzip )
87+ write ( content , cache_path ( path , extension ) , compressions : compressions )
8188 end
8289 end
8390
@@ -168,16 +175,22 @@ def delete(path)
168175
169176 File . delete ( path ) if File . exist? ( path )
170177 File . delete ( path + ".gz" ) if File . exist? ( path + ".gz" )
178+ File . delete ( path + ".br" ) if File . exist? ( path + ".br" )
171179 end
172180
173- def write ( content , path , gzip )
181+ def write ( content , path , compressions : )
174182 return unless path
175183
176184 FileUtils . makedirs ( File . dirname ( path ) )
177185 File . open ( path , "wb+" ) { |f | f . write ( content ) }
178186
179- if gzip
180- Zlib ::GzipWriter . open ( path + ".gz" , gzip ) { |f | f . write ( content ) }
187+ if compressions [ :gzip ]
188+ Zlib ::GzipWriter . open ( path + ".gz" , compressions [ :gzip ] ) { |f | f . write ( content ) }
189+ end
190+
191+ if compressions [ :brotli ]
192+ brotli = ::Brotli . deflate ( content , mode : :text , quality : compressions [ :brotli ] )
193+ File . atomic_write ( path + ".br" ) { |f | f . write ( brotli ) }
181194 end
182195 end
183196
@@ -199,9 +212,9 @@ def expire_page(path)
199212 # Manually cache the +content+ in the key determined by +path+.
200213 #
201214 # cache_page "I'm the cached content", "/lists/show"
202- def cache_page ( content , path , extension = nil , gzip = Zlib :: BEST_COMPRESSION )
215+ def cache_page ( content , path , extension = nil , compressions : COMPRESSIONS_DEFAULTS )
203216 if perform_caching
204- page_cache . cache ( content , path , extension , gzip )
217+ page_cache . cache ( content , path , extension , compressions : compressions )
205218 end
206219 end
207220
@@ -218,26 +231,50 @@ def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION)
218231 # caches_page :index, if: Proc.new { !request.format.json? }
219232 #
220233 # # don't gzip images
221- # caches_page :image, gzip : false
234+ # caches_page :image, compressions : false
222235 def caches_page ( *actions )
223236 if perform_caching
224237 options = actions . extract_options!
225238
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
239+ compressions = options . fetch ( :compressions , page_cache_compressions )
240+
241+ compressions =
242+ case compressions
233243 when false
234- nil
244+ { }
235245 else
236- Zlib :: BEST_COMPRESSION
246+ compressions
237247 end
238248
249+ if options . key? ( :gzip )
250+ ActiveSupport ::Deprecation . warn (
251+ "actionpack-page-caching now support brotli compression.\n
252+ Using gzip directly is deprecated. instead of\n caches_page :index, gzip: Zlib::BEST_COMPRESSION \n
253+ please use\n caches_page :index, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9}"
254+ )
255+
256+ gzip_level = options . fetch ( :gzip , page_cache_compression )
257+ gzip_level = \
258+ case gzip_level
259+ when Symbol
260+ Zlib . const_get ( gzip_level . upcase )
261+ when Integer
262+ gzip_level
263+ when false
264+ nil
265+ else
266+ Zlib ::BEST_COMPRESSION
267+ end
268+
269+ compressions [ :gzip ] = gzip_level
270+ end
271+
272+ if compressions . key? ( :brotli )
273+ require 'brotli'
274+ end
275+
239276 after_action ( { only : actions } . merge ( options ) ) do |c |
240- c . cache_page ( nil , nil , gzip_level )
277+ c . cache_page ( nil , nil , compressions : compressions )
241278 end
242279 end
243280 end
@@ -272,7 +309,7 @@ def expire_page(options = {})
272309 # request being handled is used.
273310 #
274311 # cache_page "I'm the cached content", controller: "lists", action: "show"
275- def cache_page ( content = nil , options = nil , gzip = Zlib :: BEST_COMPRESSION )
312+ def cache_page ( content = nil , options = nil , compressions : COMPRESSIONS_DEFAULTS )
276313 if perform_caching? && caching_allowed?
277314 path = \
278315 case options
@@ -294,7 +331,7 @@ def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION)
294331 extension = ".#{ type_symbol } "
295332 end
296333
297- page_cache . cache ( content || response . body , path , extension , gzip )
334+ page_cache . cache ( content || response . body , path , extension , compressions : compressions )
298335 end
299336 end
300337
0 commit comments