Repository: romanbsd/heroku-deflater Branch: master Commit: 2da0dcd44ed5 Files: 22 Total size: 14.9 KB Directory structure: gitextract_k81df0ef/ ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── heroku-deflater.gemspec ├── lib/ │ ├── heroku-deflater/ │ │ ├── cache_control_manager.rb │ │ ├── railtie.rb │ │ ├── serve_zipped_assets.rb │ │ ├── skip_binary.rb │ │ └── version.rb │ └── heroku-deflater.rb └── spec/ ├── cache_control_manager_spec.rb ├── fixtures/ │ ├── assets/ │ │ ├── spec.js │ │ └── spec2.js │ └── packs/ │ ├── spec.js │ └── spec2.js ├── serve_zipped_assets_spec.rb ├── skip_binary_spec.rb └── spec_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status ================================================ FILE: .rspec ================================================ --colour --format progress --tag ~slow ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'rack', '>=1.4.5' group :development do gem 'bundler' gem 'rspec' gem 'rails', '~>6.1' end ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2012 Roman Shterenzon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # heroku-deflater A simple rack middleware that enables compressing of your sprockets and webpacker assets and application responses on Heroku, while not wasting CPU cycles on pointlessly compressing images and other binary responses. It also includes code from https://github.com/mattolson/heroku_rails_deflate Before serving a file from disk to a gzip-enabled client, it will look for a precompressed file in the same location that ends in ".gz". The purpose is to avoid compressing the same file each time it is requested. ## Installing Add to your Gemfile: gem 'heroku-deflater', :group => :production ## Contributing to heroku-deflater * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. * Fork the project. * Start a feature/bugfix branch. * Commit and push until you are happy with your contribution. * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. ## Copyright Copyright (c) 2012 Roman Shterenzon. See LICENSE.txt for further details. [1]: https://github.com/rack/rack/issues/349 ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require 'bundler/gem_tasks' begin require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) rescue LoadError end task default: :spec require 'rdoc/task' Rake::RDocTask.new do |rdoc| version = File.exist?('VERSION') ? File.read('VERSION') : '' rdoc.rdoc_dir = 'rdoc' rdoc.title = "heroku-deflater #{version}" rdoc.rdoc_files.include('README*') rdoc.rdoc_files.include('lib/**/*.rb') end ================================================ FILE: VERSION ================================================ 0.6.3 ================================================ FILE: heroku-deflater.gemspec ================================================ # frozen_string_literal: true require_relative 'lib/heroku-deflater/version' Gem::Specification.new do |s| s.name = 'heroku-deflater' s.version = HerokuDeflater::VERSION s.authors = ['Roman Shterenzon'] s.email = 'romanbsd@yahoo.com' s.summary = 'Deflate assets on heroku' s.description = 'Deflate assets on Heroku' s.homepage = 'https://github.com/romanbsd/heroku-deflater' s.required_ruby_version = Gem::Requirement.new('>= 2.3.0') s.require_paths = ['lib'] s.extra_rdoc_files = [ 'LICENSE.txt', 'README.md' ] s.licenses = ['MIT'] s.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end end ================================================ FILE: lib/heroku-deflater/cache_control_manager.rb ================================================ module HerokuDeflater class CacheControlManager DEFAULT_MAX_AGE = '86400'.freeze attr_reader :app, :max_age def initialize(app) @app = app @max_age = DEFAULT_MAX_AGE end def setup_max_age(max_age) @max_age = max_age end def cache_control_headers if HerokuDeflater.rails_version_5? headers = app.config.public_file_server.headers ||= {} headers['Cache-Control'] ||= "public, max-age=#{max_age}" headers else app.config.static_cache_control ||= "public, max-age=#{max_age}" end end end end ================================================ FILE: lib/heroku-deflater/railtie.rb ================================================ require 'rack/deflater' require 'heroku-deflater/skip_binary' require 'heroku-deflater/serve_zipped_assets' require 'heroku-deflater/cache_control_manager' module HerokuDeflater class Railtie < Rails::Railtie initializer 'heroku_deflater.configure_rails_initialization' do |app| app.middleware.insert_before ActionDispatch::Static, Rack::Deflater app.middleware.insert_before ActionDispatch::Static, HerokuDeflater::SkipBinary assets_prefix = app.config.assets.prefix if app.config.respond_to?(:assets) if Object.const_defined?(:Webpacker) public_path = Webpacker.config.public_path.to_s public_output_path = Webpacker.config.public_output_path.to_s webpacker_prefix = public_output_path.split(public_path).last end if assets_prefix || webpacker_prefix paths = [assets_prefix, webpacker_prefix].compact app.middleware.insert_before Rack::Deflater, HerokuDeflater::ServeZippedAssets, app.paths['public'].first, paths, self.class.cache_control_manager(app) end end def self.cache_control_manager(app) @_cache_control_manager ||= CacheControlManager.new(app) end # Set default Cache-Control headers to 24 hours. # The configuration block in config/application.rb overrides this. config.before_initialize do |app| cache_control = cache_control_manager(app) cache_control.setup_max_age(86400) end end end ================================================ FILE: lib/heroku-deflater/serve_zipped_assets.rb ================================================ require 'action_controller' require 'action_dispatch/middleware/static' if Rails::VERSION::MAJOR < 7 # Deprecated in Rails 7.0, and removed in 7.1 require 'active_support/core_ext/uri' end # Adapted from https://gist.github.com/guyboltonking/2152663 # # Taken from: https://github.com/mattolson/heroku_rails_deflate # module HerokuDeflater class ServeZippedAssets def initialize(app, root, paths, cache_control_manager) @app = app @paths = paths.map { |p| p.chomp('/') + '/' } if HerokuDeflater.rails_version_5? @file_handler = ActionDispatch::FileHandler.new(root, headers: cache_control_manager.cache_control_headers) else @file_handler = ActionDispatch::FileHandler.new(root, cache_control_manager.cache_control_headers) end end def call(env) if env['REQUEST_METHOD'] == 'GET' request = Rack::Request.new(env) encoding = Rack::Utils.select_best_encoding(%w(gzip identity), request.accept_encoding) if encoding == 'gzip' # See if gzipped version exists in assets directory compressed_path = env['PATH_INFO'] + '.gz' if compressed_path.start_with?(*@paths) && (match = @file_handler.match?(compressed_path)) # Get the FileHandler to serve up the gzipped file, then strip the .gz suffix env['PATH_INFO'] = match status, headers, body = @file_handler.call(env) env['PATH_INFO'].chomp!('.gz') # Set the Vary HTTP header. vary = headers['Vary'].to_s.split(',').map(&:strip) unless vary.include?('*') || vary.include?('Accept-Encoding') headers['Vary'] = vary.push('Accept-Encoding').join(',') end # Add encoding and type headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = Rack::Mime.mime_type(File.extname(env['PATH_INFO']), 'text/plain') body.close if body.respond_to?(:close) return [status, headers, body] end end end @app.call(env) end end end ================================================ FILE: lib/heroku-deflater/skip_binary.rb ================================================ module HerokuDeflater # Add a no-transform Cache-Control header to binary types, so they won't get gzipped class SkipBinary def initialize(app) @app = app end WHITELIST = [ %r{^text/}, 'application/javascript', %r{^application/.*?json}, %r{^application/.*?xml}, 'application/x-font-ttf', 'font/opentype', 'image/svg+xml' ].freeze def call(env) status, headers, body = @app.call(env) headers = Rack::Utils::HeaderHash.new(headers) if defined?(Rack::Utils::HeaderHash) content_type = headers['content-type'] cache_control = headers['cache-control'].to_s.downcase unless cache_control.include?('no-transform') || WHITELIST.any? { |type| type === content_type } if cache_control.empty? headers['cache-control'] = 'no-transform' else headers['cache-control'] += ', no-transform' end end body.close if body.respond_to?(:close) [status, headers.to_h, body] end end end ================================================ FILE: lib/heroku-deflater/version.rb ================================================ # frozen_string_literal: true module HerokuDeflater VERSION = '0.7.0' end ================================================ FILE: lib/heroku-deflater.rb ================================================ require 'heroku-deflater/railtie' module HerokuDeflater def self.rails_version_5? Rails::VERSION::MAJOR >= 5 end end ================================================ FILE: spec/cache_control_manager_spec.rb ================================================ require 'spec_helper' describe HerokuDeflater::CacheControlManager do subject { described_class.new(app) } before { subject.setup_max_age(86400) } context 'Rails 4.x and below' do let(:app) { Rails4App.new } before do allow(HerokuDeflater).to receive(:rails_version_5?).and_return(false) subject.cache_control_headers end it 'sets max age for static_cache_control config option' do expect(app.config.static_cache_control).to eq('public, max-age=86400') end it 'cache_control_headers returns cache control option' do expect(subject.cache_control_headers).to eq('public, max-age=86400') end end context 'Rails 5' do let(:app) { Rails5App.new } before do allow(HerokuDeflater).to receive(:rails_version_5?).and_return(true) subject.cache_control_headers end it 'sets max age for public_file_server config option' do expect(app.config.public_file_server.headers['Cache-Control']).to eq('public, max-age=86400') end it 'cache_control_headers returns hash cache control headers' do expect(subject.cache_control_headers).to eq({'Cache-Control' =>'public, max-age=86400'}) end end end ================================================ FILE: spec/fixtures/assets/spec.js ================================================ // Fixture file (function() { console.log("Hello world"); console.log("Hello world"); console.log("Hello world"); })(); ================================================ FILE: spec/fixtures/assets/spec2.js ================================================ // Fixture file (function() { console.log("Hello world"); })(); ================================================ FILE: spec/fixtures/packs/spec.js ================================================ // Fixture file (function() { console.log("Hello world"); console.log("Hello world"); console.log("Hello world"); })(); ================================================ FILE: spec/fixtures/packs/spec2.js ================================================ // Fixture file (function() { console.log("Hello world"); })(); ================================================ FILE: spec/serve_zipped_assets_spec.rb ================================================ require 'spec_helper' describe HerokuDeflater::ServeZippedAssets do shared_examples_for 'ServeZippedAssets' do %w(assets packs).each do |path| it 'does nothing for clients which do not want gzip' do status, headers, body = process("/#{path}/spec.js", nil) expect(status).to eq(404) end it 'handles the pre-gzipped assets' do status, headers, body = process("/#{path}/spec.js") expect(status).to eq(200) end it 'has correct content type' do status, headers, body = process("/#{path}/spec.js") expect(headers['Content-Type']).to eq('application/javascript') end it 'has correct content encoding' do status, headers, body = process("/#{path}/spec.js") expect(headers['Content-Encoding']).to eq('gzip') end it 'has correct content length' do status, headers, body = process("/#{path}/spec.js") expect(headers['Content-Length']).to eq('86') end it 'has correct cache control' do status, headers, body = process("/#{path}/spec.js") expect(headers['Cache-Control']).to eq('public, max-age=86400') end it 'does not serve non-gzipped assets' do status, headers, body = process("/#{path}/spec2.js") expect(status).to eq(404) end it 'does not serve anything from non-asset directories' do status, headers, body = process("/non-#{path}/spec.js") expect(status).to eq(404) end end end describe 'Rais 4.x' do let(:app) { Rails4App.new } before do allow(HerokuDeflater).to receive(:rails_version_5?) { false } end # FIXME: It will work only with gemfiles # it_behaves_like 'ServeZippedAssets' end describe 'Rais 5.x' do let(:app) { Rails5App.new } before do allow(HerokuDeflater).to receive(:rails_version_5?) { true } end it_behaves_like 'ServeZippedAssets' end def process(path, accept_encoding = 'compress, gzip, deflate') env = Rack::MockRequest.env_for(path) env['HTTP_ACCEPT_ENCODING'] = accept_encoding middleware.call(env) end def middleware @middleware ||= begin root_path = File.expand_path('../fixtures', __FILE__) cache_control_manager = HerokuDeflater::CacheControlManager.new(app) mock = lambda { |env| [404, {'X-Cascade' => 'pass'}, []] } described_class.new(mock, root_path, ['/assets', '/packs'], cache_control_manager) end end end ================================================ FILE: spec/skip_binary_spec.rb ================================================ require 'spec_helper' describe HerokuDeflater::SkipBinary do let(:env) { Rack::MockRequest.env_for('/') } def app_returning_type(type, headers = {}) lambda { |env| [200, {'Content-Type' => type}.merge(headers), ['']] } end def process(type, headers = {}) described_class.new(app_returning_type(type, headers)).call(env)[1] end it "forbids compressing of binary types" do %w[application/gzip application/pdf image/jpeg].each do |type| headers = process(type) expect(headers['Cache-Control'].to_s).to include('no-transform') end end it "allows compressing of text types" do %w[text/plain text/html application/json application/javascript application/rss+xml application/vnd.api+json].each do |type| headers = process(type) expect(headers['Cache-Control'].to_s).not_to include('no-transform') end end it "adds to existing headers" do headers = process('image/gif', 'Cache-Control' => 'public') expect(headers['Cache-Control']).to eq('public, no-transform') end it "doesn't add 'no-transform' if it's already present" do headers = process('image/gif', 'Cache-Control' => 'public, no-transform') expect(headers['Cache-Control']).to eq('public, no-transform') end end ================================================ FILE: spec/spec_helper.rb ================================================ require 'rails' require 'rack/mock' require 'rack/static' require 'heroku-deflater' require 'heroku-deflater/serve_zipped_assets' require 'heroku-deflater/cache_control_manager' require 'heroku-deflater/skip_binary' class Rails5App def config @_config ||= Config.new end class Config attr_accessor :headers def initialize @headers = {} end def public_file_server self end end end class Rails4App def config @_config ||= Config.new end class Config attr_accessor :static_cache_control end end