Skip to content

Instantly share code, notes, and snippets.

@lilith
Created February 8, 2013 14:05
Show Gist options
  • Save lilith/4739210 to your computer and use it in GitHub Desktop.
Save lilith/4739210 to your computer and use it in GitHub Desktop.
custom rack_deflater.rb, for hardwiredcms
require "zlib"
require "stringio"
require "time" # for Time.httpdate
require 'rack/utils'
module Hardwired
class Deflater
DEFAULT_CONTENT_TYPES =
[
# All html, text, css, and csv content should be compressed
"text/plain",
"text/html",
"text/csv",
"text/css",
# Only vector graphics and uncompressed bitmaps can benefit from compression.
#GIF, JPG, and PNG already use a lz* algorithm, and certain browsers can get confused.
"image/x-icon",
"image/svg+xml",
"application/x-font-ttf",
"application/x-font-opentype",
"application/vnd.ms-fontobject",
# All javascript should be compressed
"text/javascript",
"application/ecmascript",
"application/json",
"application/javascript",
# All xml should be compressed
"text/xml",
"application/xml",
"application/xml-dtd",
"application/soap+xml",
"application/xhtml+xml",
"application/rdf+xml",
"application/rss+xml",
"application/atom+xml"]
##
# Creates Rack::Deflater middleware.
#
# [app] rack app instance
# [options] hash of deflater options, i.e.
# 'min_length' - minimum content length to trigger deflating (defaults to 1024 bytes)
# 'skip_if' - a lambda which, if evaluates to true, skips deflating
# 'include_types' - a lambda (Ruby 1.9+) or an array denoting mime-types to compress
def initialize(app, options = {})
@app = app
@min_length = options[:min_length] || 1024
@skip_if = options[:skip_if]
@include_types = options[:include_types] || DEFAULT_CONTENT_TYPES
end
def call(env)
status, headers, body = @app.call(env)
headers = Rack::Utils::HeaderHash.new(headers)
unless should_deflate?(env, status, headers, body)
return [status, headers, body]
end
request = Rack::Request.new(env)
encoding = Rack::Utils.select_best_encoding(%w(gzip deflate identity),
request.accept_encoding)
# Set the Vary HTTP header.
vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
unless vary.include?("*") || vary.include?("Accept-Encoding")
headers["Vary"] = vary.push("Accept-Encoding").join(",")
end
case encoding
when "gzip"
headers['Content-Encoding'] = "gzip"
headers.delete('Content-Length')
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
[status, headers, GzipStream.new(body, mtime)]
when "deflate"
headers['Content-Encoding'] = "deflate"
headers.delete('Content-Length')
[status, headers, DeflateStream.new(body)]
when "identity"
[status, headers, body]
when nil
body.close if body.respond_to?(:close)
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
[406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
end
end
class GzipStream
def initialize(body, mtime)
@body = body
@mtime = mtime
end
def each(&block)
@writer = block
gzip =::Zlib::GzipWriter.new(self)
gzip.mtime = @mtime
@body.each { |part|
gzip.write(part)
gzip.flush
}
ensure
@body.close if @body.respond_to?(:close)
gzip.close
@writer = nil
end
def write(data)
@writer.call(data)
end
end
class DeflateStream
DEFLATE_ARGS = [
Zlib::DEFAULT_COMPRESSION,
# drop the zlib header which causes both Safari and IE to choke
-Zlib::MAX_WBITS,
Zlib::DEF_MEM_LEVEL,
Zlib::DEFAULT_STRATEGY
]
def initialize(body)
@body = body
end
def each
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
@body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
yield deflater.finish
nil
ensure
@body.close if @body.respond_to?(:close)
deflater.close
end
end
private
def should_deflate?(env, status, headers, body)
# Skip compressing empty entity body responses and responses with
# no-transform set.
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
return false
end
# Skip if response body is too short
if @min_length > headers['Content-Length'].to_i
return false
end
# Skip if :skip_if lambda is provided and evaluates to true
if @skip_if &&
@skip_if.call(env, status, headers, body)
return false
end
mime_type = headers['Content-Type'].gsub(/;.*\Z/,"").downcase
# Skip if :include is provided and evaluates to false
if @include_types &&
!((@include_types === mime_type) ||
(@include_types.respond_to?(:"include?") &&
@include_types.include?(mime_type)))
puts "Not compressing #{mime_type}"
return false
end
true
end
end
end
@lilith
Copy link
Author

lilith commented Apr 4, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment