Merge pull request #204 from crisward/master

gzip for static files
This commit is contained in:
Serdar Dogruyol 2016-09-18 13:03:54 +03:00 committed by GitHub
commit d41ad90826
7 changed files with 224 additions and 10 deletions

View file

@ -133,4 +133,21 @@ describe "Macros" do
Kemal.config.handlers.last.is_a?(HTTP::DeflateHandler).should eq false Kemal.config.handlers.last.is_a?(HTTP::DeflateHandler).should eq false
end end
end end
describe "#serve_static" do
it "should disble static file hosting" do
serve_static false
Kemal.config.serve_static.should eq false
end
it "should disble enable gzip and dir_listing" do
serve_static({"gzip" => true, "dir_listing" => true})
conf = Kemal.config.serve_static
conf.is_a?(Hash).should eq true
if conf.is_a?(Hash)
conf["gzip"].should eq true
conf["dir_listing"].should eq true
end
end
end
end end

View file

@ -0,0 +1,9 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse posuere cursus consectetur. Donec mauris lorem, sodales a eros a, ultricies convallis ante. Quisque elementum lacus purus, sagittis mollis justo dignissim ac. Suspendisse potenti. Cras non mauris accumsan mi porttitor congue. Quisque posuere aliquam tellus sit amet ultrices. Sed at tortor sed libero fringilla luctus vitae quis magna. In maximus congue felis, et porta tortor egestas sed. Phasellus orci eros, finibus sed ipsum eget, euismod bibendum nisl. Etiam ultrices facilisis diam in gravida. Praesent lobortis leo vitae aliquet volutpat. Praesent vel blandit risus. In suscipit eget nunc at ultrices. Proin dapibus feugiat diam ut tincidunt. Donec lectus diam, ornare ut consequat nec, gravida sit amet metus.
Nunc a viverra urna, quis ullamcorper augue. Morbi posuere auctor nibh, tempor luctus massa mollis laoreet. Pellentesque sagittis leo eu felis interdum finibus. Pellentesque porttitor lobortis arcu, eu mollis dui iaculis nec. Vestibulum sit amet sodales erat. Nullam quis mi massa. Suspendisse sit amet elit auctor, feugiat ipsum a, placerat metus. Vestibulum quis felis a lectus blandit aliquam. Nam consectetur iaculis nulla. Mauris sit amet condimentum erat, in vestibulum dui. Nullam nec mattis tortor, non viverra nunc. Proin eget congue augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed ut hendrerit nulla. Etiam cursus sagittis metus, et feugiat ligula molestie sit amet. Aliquam laoreet auctor sagittis.
Aliquam tempor urna non consectetur tincidunt. Maecenas porttitor augue diam, ac lobortis nulla suscipit eget. Ut quis lacus facilisis, euismod lacus non, ullamcorper urna. Cras pretium fringilla pharetra. Praesent sed nunc at elit vulputate elementum. Suspendisse ac molestie nunc, sit amet consectetur nunc. Cras placerat ligula tortor, non bibendum massa tempus ut. Etiam eros erat, gravida id felis eget, congue suscipit ipsum. Sed condimentum erat at facilisis dictum. Cras venenatis vitae turpis vitae sagittis. Proin id posuere est, non ornare sem. Donec vitae sollicitudin dolor, a pulvinar ex. Integer porta velit lectus, et imperdiet enim commodo a.
Donec sit amet ipsum tempus, tincidunt neque eget, luctus massa. Praesent vel nulla pretium, bibendum enim a, pulvinar enim. Vestibulum non libero eu est dignissim cursus. Nullam commodo tellus imperdiet feugiat placerat. Sed sed dolor ut nibh blandit maximus ac eget neque. Ut sit amet augue maximus, lacinia eros non, faucibus eros. Suspendisse ac bibendum libero, eu lobortis nulla. Mauris arcu nulla, tempus eu varius eu, bibendum at nibh. Donec id libero consequat, volutpat ex vitae, molestie velit. Aliquam aliquam sem ac arcu pellentesque, placerat bibendum enim dapibus. Duis consectetur ligula non placerat euismod.
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin commodo ullamcorper venenatis. Cras ac lorem sit amet augue varius convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris dolor nisi, efficitur id aliquet ut, ultricies sed elit. Proin ultricies turpis dolor, in auctor velit aliquet nec. Praesent vehicula aliquam viverra. Suspendisse potenti. Donec aliquet iaculis ultricies. Proin dignissim vitae nisl at rutrum.

2
spec/static/dir/test.txt Normal file
View file

@ -0,0 +1,2 @@
hello
world

102
spec/static_file_handler.cr Normal file
View file

@ -0,0 +1,102 @@
require "./spec_helper"
private def handle(request, fallthrough = true)
io = MemoryIO.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
handler = Kemal::StaticFileHandler.new "#{__DIR__}/static", fallthrough
handler.call context
response.close
io.rewind
HTTP::Client::Response.from_io(io)
end
describe Kemal::StaticFileHandler do
file_text = File.read "#{__DIR__}/static/dir/test.txt"
it "should serve a file with content type and etag" do
response = handle HTTP::Request.new("GET", "/dir/test.txt")
response.status_code.should eq(200)
response.headers["Content-Type"].should eq "text/plain"
response.headers["Etag"].should contain "W/\""
response.body.should eq(File.read("#{__DIR__}/static/dir/test.txt"))
end
it "should respond with 304 if file has not changed" do
response = handle HTTP::Request.new("GET", "/dir/test.txt")
response.status_code.should eq(200)
etag = response.headers["Etag"]
headers = HTTP::Headers{"If-None-Match" => etag}
response = handle HTTP::Request.new("GET", "/dir/test.txt", headers)
response.status_code.should eq(304)
response.body.should eq ""
end
it "should not list directory's entries" do
serve_static({"gzip" => true, "dir_listing" => false})
response = handle HTTP::Request.new("GET", "/dir/")
response.status_code.should eq(404)
end
it "should list directory's entries when config is set" do
serve_static({"gzip" => true, "dir_listing" => true})
response = handle HTTP::Request.new("GET", "/dir/")
response.status_code.should eq(200)
response.body.should match(/test.txt/)
end
it "should gzip a file if config is true, headers accept gzip and file is > 880 bytes" do
serve_static({"gzip" => true, "dir_listing" => true})
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers)
response.status_code.should eq(200)
response.headers["Content-Encoding"].should eq "gzip"
end
it "should not gzip a file if config is true, headers accept gzip and file is < 880 bytes" do
serve_static({"gzip" => true, "dir_listing" => true})
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
response = handle HTTP::Request.new("GET", "/dir/test.txt", headers)
response.status_code.should eq(200)
response.headers["Content-Encoding"]?.should eq nil
end
it "should not gzip a file if config is false, headers accept gzip and file is > 880 bytes" do
serve_static({"gzip" => false, "dir_listing" => true})
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers)
response.status_code.should eq(200)
response.headers["Content-Encoding"]?.should eq nil
end
it "should not serve a not found file" do
response = handle HTTP::Request.new("GET", "/not_found_file.txt")
response.status_code.should eq(404)
end
it "should not serve a not found directory" do
response = handle HTTP::Request.new("GET", "/not_found_dir/")
response.status_code.should eq(404)
end
it "should not serve a file as directory" do
response = handle HTTP::Request.new("GET", "/dir/test.txt/")
response.status_code.should eq(404)
end
it "should handle only GET and HEAD method" do
%w(GET HEAD).each do |method|
response = handle HTTP::Request.new(method, "/dir/test.txt")
response.status_code.should eq(200)
end
%w(POST PUT DELETE).each do |method|
response = handle HTTP::Request.new(method, "/dir/test.txt")
response.status_code.should eq(404)
response = handle HTTP::Request.new(method, "/dir/test.txt"), false
response.status_code.should eq(405)
response.headers["Allow"].should eq("GET, HEAD")
end
end
end

View file

@ -15,13 +15,13 @@ module Kemal
{% end %} {% end %}
property host_binding, ssl, port, env, public_folder, logging, property host_binding, ssl, port, env, public_folder, logging,
always_rescue, serve_static, server, extra_options always_rescue, serve_static : (Bool | Hash(String, Bool)), server, extra_options
def initialize def initialize
@host_binding = "0.0.0.0" @host_binding = "0.0.0.0"
@port = 3000 @port = 3000
@env = "development" @env = "development"
@serve_static = true @serve_static = {"dir_listing" => false, "gzip" => true}
@public_folder = "./public" @public_folder = "./public"
@logging = true @logging = true
@logger = nil @logger = nil
@ -93,7 +93,7 @@ module Kemal
end end
private def setup_static_file_handler private def setup_static_file_handler
HANDLERS.insert(3, Kemal::StaticFileHandler.new(@public_folder)) if @serve_static HANDLERS.insert(3, Kemal::StaticFileHandler.new(@public_folder)) if @serve_static.is_a?(Hash)
end end
end end

View file

@ -32,7 +32,7 @@ def logger(logger)
end end
# Enables / Disables static file serving. # Enables / Disables static file serving.
def serve_static(status) def serve_static(status : (Bool | Hash))
Kemal.config.serve_static = status Kemal.config.serve_static = status
end end

View file

@ -1,13 +1,92 @@
{% if !flag?(:without_zlib) %}
require "zlib"
{% end %}
module Kemal module Kemal
# Kemal::StaticFileHandler is used to serve static files(.js/.css/.png e.g).
# This handler is on by default and you can disable it like.
#
# serve_static false
#
class StaticFileHandler < HTTP::StaticFileHandler class StaticFileHandler < HTTP::StaticFileHandler
def call(context) def call(context)
return call_next(context) if context.request.path.not_nil! == "/" return call_next(context) if context.request.path.not_nil! == "/"
super
unless context.request.method == "GET" || context.request.method == "HEAD"
if @fallthrough
call_next(context)
else
context.response.status_code = 405
context.response.headers.add("Allow", "GET, HEAD")
end
return
end
config = Kemal.config.serve_static
original_path = context.request.path.not_nil!
is_dir_path = original_path.ends_with? "/"
request_path = URI.unescape(original_path)
# File path cannot contains '\0' (NUL) because all filesystem I know
# don't accept '\0' character as file name.
if request_path.includes? '\0'
context.response.status_code = 400
return
end
expanded_path = File.expand_path(request_path, "/")
if is_dir_path && !expanded_path.ends_with? "/"
expanded_path = "#{expanded_path}/"
end
is_dir_path = expanded_path.ends_with? "/"
file_path = File.join(@public_dir, expanded_path)
is_dir = Dir.exists? file_path
if request_path != expanded_path || is_dir && !is_dir_path
redirect_to context, "#{expanded_path}#{is_dir && !is_dir_path ? "/" : ""}"
end
if Dir.exists?(file_path)
if config.is_a?(Hash) && config["dir_listing"] == true
context.response.content_type = "text/html"
directory_listing(context.response, request_path, file_path)
else
return call_next(context)
end
elsif File.exists?(file_path)
return if self.etag(context, file_path)
minsize = 860 # http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits ??
context.response.content_type = mime_type(file_path)
request_headers = context.request.headers
filesize = File.size(file_path)
File.open(file_path) do |file|
if request_headers.includes_word?("Accept-Encoding", "gzip") && config.is_a?(Hash) && config["gzip"] == true && filesize > minsize && self.zip_types(file_path)
context.response.headers["Content-Encoding"] = "gzip"
Zlib::Deflate.gzip(context.response) do |deflate|
IO.copy(file, deflate)
end
elsif request_headers.includes_word?("Accept-Encoding", "deflate") && config.is_a?(Hash) && config["gzip"]? == true && filesize > minsize && self.zip_types(file_path)
context.response.headers["Content-Encoding"] = "deflate"
Zlib::Deflate.new(context.response) do |deflate|
IO.copy(file, deflate)
end
else
context.response.content_length = filesize
IO.copy(file, context.response)
end
end
else
call_next(context)
end
end
def etag(context, file_path)
etag = %{W/"#{File.lstat(file_path).mtime.epoch.to_s}"}
context.response.headers["ETag"] = etag
return false if !context.request.headers["If-None-Match"]? || context.request.headers["If-None-Match"] != etag
context.response.content_length = 0
context.response.status_code = 304 # not modified
return true
end
def zip_types(path) # https://github.com/h5bp/server-configs-nginx/blob/master/nginx.conf
[".htm", ".html", ".txt", ".css", ".js", ".svg", ".json", ".xml", ".otf", ".ttf", ".woff", ".woff2"].includes? File.extname(path)
end end
def mime_type(path) def mime_type(path)
@ -20,6 +99,11 @@ module Kemal
when ".jpg", ".jpeg" then "image/jpeg" when ".jpg", ".jpeg" then "image/jpeg"
when ".gif" then "image/gif" when ".gif" then "image/gif"
when ".svg" then "image/svg+xml" when ".svg" then "image/svg+xml"
when ".xml" then "application/xml"
when ".json" then "application/json"
when ".otf", ".ttf" then "application/font-sfnt"
when ".woff" then "application/font-woff"
when ".woff2" then "font/woff2"
else "application/octet-stream" else "application/octet-stream"
end end
end end