Skip to content

Enable brotli compression in the caches_page #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ cache:
bundler: true

before_install:
- gem install bundler
- "travis_retry gem update --system 2.7.9"
- "travis_retry gem install bundler -v '1.17.3'"
- "travis_retry gem install brotli"

rvm:
- 2.4.6
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.2.4 Unreleased

* Allow to use brotli compression

*Anton Kolodii*

## 1.1.1 (September 25, 2018)

* Fixes handling of several forward slashes as root path.
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,37 @@ Additionally, you can expire caches using
that act on changes in the model to determine when a cache is supposed to be
expired.

#### Compression Algorithm

To use brotli manually install gem 'brotli' inside Gemfile
``` ruby
gem 'brotli'
```

To use both gzip and brotli, or just skip compressions, because this is default value

``` ruby
class WeblogController < ActionController::Base
caches_page :show, :new, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9}
end
```

To use only gzip

``` ruby
class WeblogController < ActionController::Base
caches_page :show, :new, compressions: {gzip: Zlib::BEST_COMPRESSION}
end
```

To use only brotli

``` ruby
class WeblogController < ActionController::Base
caches_page :show, :new, compressions: {brotli: 9}
end
```

Contributing
------------

Expand Down
3 changes: 3 additions & 0 deletions actionpack-page_caching.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ Gem::Specification.new do |gem|

gem.add_dependency "actionpack", ">= 5.0.0"

gem.add_development_dependency 'brotli', '>= 0.2.0'
gem.add_development_dependency "mocha"

gem.post_install_message = "To use brotli compression you have to manually add gem 'brotli' to Gemfile"
end
77 changes: 57 additions & 20 deletions lib/action_controller/caching/pages.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# frozen_string_literal: true

require "fileutils"
require "uri"
require "active_support/core_ext/class/attribute_accessors"
require "active_support/core_ext/string/strip"

module ActionController
module Caching
COMPRESSIONS_DEFAULTS = {gzip: 9, brotli: 9} # ::Zlib::BEST_COMPRESSION

# Page caching is an approach to caching where the entire action output of is
# stored as a HTML file that the web server can serve without going through
# Action Pack. This is the fastest way to cache your content as opposed to going
Expand Down Expand Up @@ -60,6 +64,9 @@ module Pages
# or <tt>:best_speed</tt> or an integer configuring the compression level.
class_attribute :page_cache_compression
self.page_cache_compression ||= false

class_attribute :page_cache_compressions
self.page_cache_compressions ||= COMPRESSIONS_DEFAULTS
end

class PageCache #:nodoc:
Expand All @@ -75,9 +82,9 @@ def expire(path)
end
end

def cache(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION)
def cache(content, path, extension = nil, compressions: COMPRESSIONS_DEFAULTS)
instrument :write_page, path do
write(content, cache_path(path, extension), gzip)
write(content, cache_path(path, extension), compressions: compressions)
end
end

Expand Down Expand Up @@ -168,16 +175,22 @@ def delete(path)

File.delete(path) if File.exist?(path)
File.delete(path + ".gz") if File.exist?(path + ".gz")
File.delete(path + ".br") if File.exist?(path + ".br")
end

def write(content, path, gzip)
def write(content, path, compressions:)
return unless path

FileUtils.makedirs(File.dirname(path))
File.open(path, "wb+") { |f| f.write(content) }

if gzip
Zlib::GzipWriter.open(path + ".gz", gzip) { |f| f.write(content) }
if compressions[:gzip]
Zlib::GzipWriter.open(path + ".gz", compressions[:gzip]) { |f| f.write(content) }
end

if compressions[:brotli]
brotli = ::Brotli.deflate(content, mode: :text, quality: compressions[:brotli])
File.atomic_write(path + ".br") { |f| f.write(brotli) }
end
end

Expand All @@ -199,9 +212,9 @@ def expire_page(path)
# Manually cache the +content+ in the key determined by +path+.
#
# cache_page "I'm the cached content", "/lists/show"
def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION)
def cache_page(content, path, extension = nil, compressions: COMPRESSIONS_DEFAULTS)
if perform_caching
page_cache.cache(content, path, extension, gzip)
page_cache.cache(content, path, extension, compressions: compressions)
end
end

Expand All @@ -218,26 +231,50 @@ def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION)
# caches_page :index, if: Proc.new { !request.format.json? }
#
# # don't gzip images
# caches_page :image, gzip: false
# caches_page :image, compressions: false
def caches_page(*actions)
if perform_caching
options = actions.extract_options!

gzip_level = options.fetch(:gzip, page_cache_compression)
gzip_level = \
case gzip_level
when Symbol
Zlib.const_get(gzip_level.upcase)
when Integer
gzip_level
compressions = options.fetch(:compressions, page_cache_compressions)

compressions =
case compressions
when false
nil
{}
else
Zlib::BEST_COMPRESSION
compressions
end

if options.key?(:gzip)
ActiveSupport::Deprecation.warn(
"actionpack-page-caching now support brotli compression.\n
Using gzip directly is deprecated. instead of\n caches_page :index, gzip: Zlib::BEST_COMPRESSION \n
please use\n caches_page :index, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9}"
)

gzip_level = options.fetch(:gzip, page_cache_compression)
gzip_level = \
case gzip_level
when Symbol
Zlib.const_get(gzip_level.upcase)
when Integer
gzip_level
when false
nil
else
Zlib::BEST_COMPRESSION
end

compressions[:gzip] = gzip_level
end

if compressions.key?(:brotli)
require 'brotli'
end

after_action({ only: actions }.merge(options)) do |c|
c.cache_page(nil, nil, gzip_level)
c.cache_page(nil, nil, compressions: compressions)
end
end
end
Expand Down Expand Up @@ -272,7 +309,7 @@ def expire_page(options = {})
# request being handled is used.
#
# cache_page "I'm the cached content", controller: "lists", action: "show"
def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION)
def cache_page(content = nil, options = nil, compressions: COMPRESSIONS_DEFAULTS)
if perform_caching? && caching_allowed?
path = \
case options
Expand All @@ -294,7 +331,7 @@ def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION)
extension = ".#{type_symbol}"
end

page_cache.cache(content || response.body, path, extension, gzip)
page_cache.cache(content || response.body, path, extension, compressions: compressions)
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/action_controller/page_caching.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require "action_controller/caching/pages"

module ActionController
Expand Down
2 changes: 2 additions & 0 deletions lib/actionpack/page_caching.rb
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# frozen_string_literal: true

require "actionpack/page_caching/railtie"
2 changes: 2 additions & 0 deletions lib/actionpack/page_caching/railtie.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require "rails/railtie"

module ActionPack
Expand Down
50 changes: 40 additions & 10 deletions test/caching_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ class PageCachingTestController < CachingController
caches_page :ok, :no_content, if: Proc.new { |c| !c.request.format.json? }
caches_page :found, :not_found
caches_page :about_me
caches_page :default_gzip
caches_page :no_gzip, gzip: false
caches_page :gzip_level, gzip: :best_speed
caches_page :default_compressions
caches_page :no_gzip, compressions: {gzip: false}
caches_page :gzip_level, compressions: {gzip: 1}
caches_page :no_brotli, compressions: {brotli: false}
caches_page :brotli_level, compressions: {brotli: 1}

def ok
render html: "ok"
Expand All @@ -144,8 +146,8 @@ def custom_path
cache_page(nil, "/index.html")
end

def default_gzip
render html: "default_gzip"
def default_compressions
render html: "default_compressions"
end

def no_gzip
Expand All @@ -156,6 +158,14 @@ def gzip_level
render html: "gzip_level"
end

def no_brotli
render html: "no_brotli"
end

def brotli_level
render html: "brotli_level"
end

def expire_custom_path
expire_page("/index.html")
head :ok
Expand Down Expand Up @@ -261,6 +271,7 @@ def test_should_gzip_cache

get :expire_custom_path
assert_page_not_cached :index, controller: ".", format: "html.gz"
assert_page_not_cached :index, controller: ".", format: "html.br"
end

def test_should_allow_to_disable_gzip
Expand All @@ -273,24 +284,43 @@ def test_should_allow_to_disable_gzip
assert_page_not_cached :no_gzip, format: "html.gz"
end

def test_should_use_config_gzip_by_default
def test_should_allow_to_disable_brotli
draw do
get "/page_caching_test/no_brotli", to: "page_caching_test#no_brotli"
end

get :no_brotli
assert_page_cached :no_brotli, format: "html"
assert_page_not_cached :no_brotli, format: "html.br"
end

def test_should_use_config_compressions_by_default
draw do
get "/page_caching_test/default_gzip", to: "page_caching_test#default_gzip"
get "/page_caching_test/default_compressions", to: "page_caching_test#default_compressions"
end

@controller.expects(:cache_page).with(nil, nil, Zlib::BEST_COMPRESSION)
get :default_gzip
@controller.expects(:cache_page).with(nil, nil, compressions: {gzip: Zlib::BEST_COMPRESSION, brotli: 9})
get :default_compressions
end

def test_should_set_gzip_level
draw do
get "/page_caching_test/gzip_level", to: "page_caching_test#gzip_level"
end

@controller.expects(:cache_page).with(nil, nil, Zlib::BEST_SPEED)
@controller.expects(:cache_page).with(nil, nil, compressions: {gzip: Zlib::BEST_SPEED})
get :gzip_level
end

def test_should_set_brotli_level
draw do
get "/page_caching_test/brotli_level", to: "page_caching_test#brotli_level"
end

@controller.expects(:cache_page).with(nil, nil, compressions: {brotli: 1})
get :brotli_level
end

def test_should_cache_without_trailing_slash_on_url
@controller.class.cache_page "cached content", "/page_caching_test/trailing_slash"
assert_page_cached :trailing_slash, content: "cached content"
Expand Down