Compare commits
No commits in common. "99b1e759ba429f10095ab06a2977636fd3392721" and "eb1832acaf1e9ce1014b99d3bd56ec8522d0df91" have entirely different histories.
99b1e759ba
...
eb1832acaf
18 changed files with 55 additions and 145 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: sdogruyol
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: sdogruyol
|
patreon: sdogruyol
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,24 +1,3 @@
|
||||||
# 1.2.0 (07-07-2022)
|
|
||||||
|
|
||||||
- Crystal 1.5.0 support :tada:
|
|
||||||
- Eliminated several seconds of delay when loading big mp4 file. Thanks @Athlon64 :pray:
|
|
||||||
- Fix content_for failing to capture the correct block input [#639](https://github.com/kemalcr/kemal/pull/639). Thanks @sdogruyol :pray:
|
|
||||||
- Closes response by default in HTTP::Server::Context#redirect [#641](https://github.com/kemalcr/kemal/pull/641). Thanks @cyangle :pray:
|
|
||||||
- Enable option for index.html to be a directories default [#640](https://github.com/kemalcr/kemal/pull/640). Thanks @ukd1 :pray:
|
|
||||||
|
|
||||||
You can enable it via
|
|
||||||
|
|
||||||
```crystal
|
|
||||||
serve_static({"dir_index" => true})
|
|
||||||
```
|
|
||||||
|
|
||||||
# 1.1.2 (24-02-2022)
|
|
||||||
|
|
||||||
- Fix content rendering [#631](https://github.com/kemalcr/kemal/pull/631). Thanks @matthewmcgarvey :pray:
|
|
||||||
|
|
||||||
# 1.1.1 (22-02-2022)
|
|
||||||
|
|
||||||
- Ignore HTTP::Server::Response patching for crystal >= 1.3.0 [#628](https://github.com/kemalcr/kemal/pull/628). Thanks @SamantazFox :pray:
|
|
||||||
# 1.1.0 (02-09-2021)
|
# 1.1.0 (02-09-2021)
|
||||||
|
|
||||||
- You can now set your own application name for startup message [#606](https://github.com/kemalcr/kemal/pull/606). Thanks @aravindavk :pray:
|
- You can now set your own application name for startup message [#606](https://github.com/kemalcr/kemal/pull/606). Thanks @aravindavk :pray:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: kemal
|
name: kemal
|
||||||
version: 1.2.0
|
version: 1.1.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Serdar Dogruyol <dogruyolserdar@gmail.com>
|
- Serdar Dogruyol <dogruyolserdar@gmail.com>
|
||||||
|
@ -15,7 +15,7 @@ dependencies:
|
||||||
development_dependencies:
|
development_dependencies:
|
||||||
ameba:
|
ameba:
|
||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
version: ~> 1.0
|
version: ~> 0.14.0
|
||||||
|
|
||||||
crystal: ">= 0.36.0"
|
crystal: ">= 0.36.0"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Hello <%= name %>
|
Hello <%= name %>
|
||||||
|
|
||||||
<% content_for "meta" do %>
|
<% content_for "custom" do %>
|
||||||
<title>Kemal Spec</title>
|
<h1>Hello from otherside</h1>
|
||||||
<% end %>
|
<% end %>
|
|
@ -1,8 +1,6 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<%= yield_content "meta" %>
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
|
<%= yield_content "custom" %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,10 +1,8 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
|
||||||
<%= yield_content "meta" %>
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
<%= var1 %>
|
<%= yield_content "custom" %>
|
||||||
<%= var2 %>
|
<%= var1 %>
|
||||||
|
<%= var2 %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -143,40 +143,4 @@ describe "Kemal::RouteHandler" do
|
||||||
client_response.body.should eq("Redirecting to /login")
|
client_response.body.should eq("Redirecting to /login")
|
||||||
client_response.headers.has_key?("Location").should eq(true)
|
client_response.headers.has_key?("Location").should eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects and closes response in before filter" do
|
|
||||||
filter_handler = Kemal::FilterHandler.new
|
|
||||||
filter_handler._add_route_filter("GET", "/", :before) do |env|
|
|
||||||
env.redirect "/login"
|
|
||||||
end
|
|
||||||
Kemal.config.add_filter_handler(filter_handler)
|
|
||||||
|
|
||||||
get "/" do
|
|
||||||
"home page"
|
|
||||||
end
|
|
||||||
|
|
||||||
request = HTTP::Request.new("GET", "/")
|
|
||||||
client_response = call_request_on_app(request)
|
|
||||||
client_response.status_code.should eq(302)
|
|
||||||
client_response.body.should eq("")
|
|
||||||
client_response.headers.has_key?("Location").should eq(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "redirects in before filter without closing response" do
|
|
||||||
filter_handler = Kemal::FilterHandler.new
|
|
||||||
filter_handler._add_route_filter("GET", "/", :before) do |env|
|
|
||||||
env.redirect "/login", close: false
|
|
||||||
end
|
|
||||||
Kemal.config.add_filter_handler(filter_handler)
|
|
||||||
|
|
||||||
get "/" do
|
|
||||||
"home page"
|
|
||||||
end
|
|
||||||
|
|
||||||
request = HTTP::Request.new("GET", "/")
|
|
||||||
client_response = call_request_on_app(request)
|
|
||||||
client_response.status_code.should eq(302)
|
|
||||||
client_response.body.should eq("home page")
|
|
||||||
client_response.headers.has_key?("Location").should eq(true)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,8 +4,8 @@ require "../src/*"
|
||||||
include Kemal
|
include Kemal
|
||||||
|
|
||||||
class CustomLogHandler < Kemal::BaseLogHandler
|
class CustomLogHandler < Kemal::BaseLogHandler
|
||||||
def call(context)
|
def call(env)
|
||||||
call_next(context)
|
call_next env
|
||||||
end
|
end
|
||||||
|
|
||||||
def write(message)
|
def write(message)
|
||||||
|
|
|
@ -23,15 +23,6 @@ describe Kemal::StaticFileHandler do
|
||||||
response.body.should eq(File.read("#{__DIR__}/static/dir/test.txt"))
|
response.body.should eq(File.read("#{__DIR__}/static/dir/test.txt"))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should serve the 'index.html' file when a directory is requested and index serving is enabled" do
|
|
||||||
serve_static({"dir_index" => true})
|
|
||||||
response = handle HTTP::Request.new("GET", "/dir/")
|
|
||||||
response.status_code.should eq(200)
|
|
||||||
response.headers["Content-Type"].should eq "text/html"
|
|
||||||
response.headers["Etag"].should contain "W/\""
|
|
||||||
response.body.should eq(File.read("#{__DIR__}/static/dir/index.html"))
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should respond with 304 if file has not changed" do
|
it "should respond with 304 if file has not changed" do
|
||||||
response = handle HTTP::Request.new("GET", "/dir/test.txt")
|
response = handle HTTP::Request.new("GET", "/dir/test.txt")
|
||||||
response.status_code.should eq(200)
|
response.status_code.should eq(200)
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe "Views" do
|
||||||
end
|
end
|
||||||
request = HTTP::Request.new("GET", "/view/world")
|
request = HTTP::Request.new("GET", "/view/world")
|
||||||
client_response = call_request_on_app(request)
|
client_response = call_request_on_app(request)
|
||||||
client_response.body.strip.should eq("<html>Hello world\n</html>")
|
client_response.body.should contain("Hello world")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders layout" do
|
it "renders layout" do
|
||||||
|
@ -56,17 +56,7 @@ describe "Views" do
|
||||||
end
|
end
|
||||||
request = HTTP::Request.new("GET", "/view/world")
|
request = HTTP::Request.new("GET", "/view/world")
|
||||||
client_response = call_request_on_app(request)
|
client_response = call_request_on_app(request)
|
||||||
client_response.body.scan("Hello world").size.should eq(1)
|
client_response.body.should contain("Hello world")
|
||||||
client_response.body.should contain("<title>Kemal Spec</title>")
|
client_response.body.should contain("<h1>Hello from otherside</h1>")
|
||||||
end
|
|
||||||
|
|
||||||
it "does not render content_for that was not yielded" do
|
|
||||||
get "/view/:name" do |env|
|
|
||||||
name = env.params.url["name"]
|
|
||||||
render "#{__DIR__}/asset/hello_with_content_for.ecr", "#{__DIR__}/asset/layout.ecr"
|
|
||||||
end
|
|
||||||
request = HTTP::Request.new("GET", "/view/world")
|
|
||||||
client_response = call_request_on_app(request)
|
|
||||||
client_response.body.should_not contain("<h1>Hello from otherside</h1>")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
14
src/kemal.cr
14
src/kemal.cr
|
@ -7,18 +7,18 @@ require "./kemal/helpers/*"
|
||||||
|
|
||||||
module Kemal
|
module Kemal
|
||||||
# Overload of `self.run` with the default startup logging.
|
# Overload of `self.run` with the default startup logging.
|
||||||
def self.run(port : Int32?, args = ARGV, trap_signal : Bool = true)
|
def self.run(port : Int32?, args = ARGV)
|
||||||
self.run(port, args, trap_signal) { }
|
self.run(port, args) { }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Overload of `self.run` without port.
|
# Overload of `self.run` without port.
|
||||||
def self.run(args = ARGV, trap_signal : Bool = true)
|
def self.run(args = ARGV)
|
||||||
self.run(nil, args: args, trap_signal: trap_signal)
|
self.run(nil, args: args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Overload of `self.run` to allow just a block.
|
# Overload of `self.run` to allow just a block.
|
||||||
def self.run(args = ARGV, &block)
|
def self.run(args = ARGV, &block)
|
||||||
self.run(nil, args: args, trap_signal: true, &block)
|
self.run(nil, args: args, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The command to run a `Kemal` application.
|
# The command to run a `Kemal` application.
|
||||||
|
@ -27,7 +27,7 @@ module Kemal
|
||||||
#
|
#
|
||||||
# To use custom command line arguments, set args to nil
|
# To use custom command line arguments, set args to nil
|
||||||
#
|
#
|
||||||
def self.run(port : Int32? = nil, args = ARGV, trap_signal : Bool = true, &block)
|
def self.run(port : Int32? = nil, args = ARGV, &block)
|
||||||
Kemal::CLI.new args
|
Kemal::CLI.new args
|
||||||
config = Kemal.config
|
config = Kemal.config
|
||||||
config.setup
|
config.setup
|
||||||
|
@ -36,7 +36,7 @@ module Kemal
|
||||||
# Test environment doesn't need to have signal trap and logging.
|
# Test environment doesn't need to have signal trap and logging.
|
||||||
if config.env != "test"
|
if config.env != "test"
|
||||||
setup_404
|
setup_404
|
||||||
setup_trap_signal if trap_signal
|
setup_trap_signal
|
||||||
end
|
end
|
||||||
|
|
||||||
server = config.server ||= HTTP::Server.new(config.handlers)
|
server = config.server ||= HTTP::Server.new(config.handlers)
|
||||||
|
|
|
@ -31,7 +31,7 @@ module Kemal
|
||||||
@host_binding = "0.0.0.0"
|
@host_binding = "0.0.0.0"
|
||||||
@port = 3000
|
@port = 3000
|
||||||
@env = ENV["KEMAL_ENV"]? || "development"
|
@env = ENV["KEMAL_ENV"]? || "development"
|
||||||
@serve_static = {"dir_listing" => false, "gzip" => true, "dir_index" => false}
|
@serve_static = {"dir_listing" => false, "gzip" => true}
|
||||||
@public_folder = "./public"
|
@public_folder = "./public"
|
||||||
@logging = true
|
@logging = true
|
||||||
@logger = nil
|
@logger = nil
|
||||||
|
|
|
@ -17,11 +17,10 @@ class HTTP::Server
|
||||||
@params ||= Kemal::ParamParser.new(@request, route_lookup.params)
|
@params ||= Kemal::ParamParser.new(@request, route_lookup.params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect(url : String, status_code : Int32 = 302, *, body : String? = nil, close : Bool = true)
|
def redirect(url : String, status_code : Int32 = 302, *, body : String? = nil)
|
||||||
@response.headers.add "Location", url
|
@response.headers.add "Location", url
|
||||||
@response.status_code = status_code
|
@response.status_code = status_code
|
||||||
@response.print(body) if body
|
@response.print(body) if body
|
||||||
@response.close if close
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def route
|
def route
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
# This override collides with the new stdlib of Crystal 1.3
|
|
||||||
# See https://github.com/kemalcr/kemal/issues/627 for more details
|
|
||||||
{{ skip_file if compare_versions(Crystal::VERSION, "1.3.0") >= 0 }}
|
|
||||||
|
|
||||||
class HTTP::Server::Response
|
class HTTP::Server::Response
|
||||||
class Output
|
class Output
|
||||||
def close
|
def close
|
||||||
|
|
|
@ -27,8 +27,8 @@ module Kemal
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(context : HTTP::Server::Context)
|
def call(env : HTTP::Server::Context)
|
||||||
call_next(context)
|
call_next(env)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Processes the path based on `only` paths which is a `Array(String)`.
|
# Processes the path based on `only` paths which is a `Array(String)`.
|
||||||
|
|
|
@ -216,7 +216,20 @@ private def multipart(file, env : HTTP::Server::Context)
|
||||||
env.response.headers["Accept-Ranges"] = "bytes"
|
env.response.headers["Accept-Ranges"] = "bytes"
|
||||||
env.response.headers["Content-Range"] = "bytes #{startb}-#{endb}/#{fileb}" # MUST
|
env.response.headers["Content-Range"] = "bytes #{startb}-#{endb}/#{fileb}" # MUST
|
||||||
|
|
||||||
file.seek(startb)
|
if startb > 1024
|
||||||
|
skipped = 0_i64
|
||||||
|
# file.skip only accepts values less or equal to 1024 (buffer size, undocumented)
|
||||||
|
until (increase_skipped = skipped + 1024_i64) > startb
|
||||||
|
file.skip(1024)
|
||||||
|
skipped = increase_skipped
|
||||||
|
end
|
||||||
|
if (skipped_minus_startb = skipped - startb) > 0
|
||||||
|
file.skip skipped_minus_startb
|
||||||
|
end
|
||||||
|
else
|
||||||
|
file.skip(startb)
|
||||||
|
end
|
||||||
|
|
||||||
IO.copy(file, env.response, content_length)
|
IO.copy(file, env.response, content_length)
|
||||||
else
|
else
|
||||||
env.response.content_length = fileb
|
env.response.content_length = fileb
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(Nil))).new
|
CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(String))).new
|
||||||
|
|
||||||
# `content_for` is a set of helpers that allows you to capture
|
# `content_for` is a set of helpers that allows you to capture
|
||||||
# blocks inside views to be rendered later during the request. The most
|
# blocks inside views to be rendered later during the request. The most
|
||||||
|
@ -34,7 +34,13 @@ CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(Nil))).new
|
||||||
# layout, inside the <head> tag, and each view can call `content_for`
|
# layout, inside the <head> tag, and each view can call `content_for`
|
||||||
# setting the appropriate set of tags that should be added to the layout.
|
# setting the appropriate set of tags that should be added to the layout.
|
||||||
macro content_for(key, file = __FILE__)
|
macro content_for(key, file = __FILE__)
|
||||||
CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, ->() { {{ yield }} }
|
%proc = ->() {
|
||||||
|
__view_io__ = IO::Memory.new
|
||||||
|
{{ yield }}
|
||||||
|
__view_io__.to_s
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -43,14 +49,7 @@ macro yield_content(key)
|
||||||
if CONTENT_FOR_BLOCKS.has_key?({{key}})
|
if CONTENT_FOR_BLOCKS.has_key?({{key}})
|
||||||
__caller_filename__ = CONTENT_FOR_BLOCKS[{{key}}][0]
|
__caller_filename__ = CONTENT_FOR_BLOCKS[{{key}}][0]
|
||||||
%proc = CONTENT_FOR_BLOCKS[{{key}}][1]
|
%proc = CONTENT_FOR_BLOCKS[{{key}}][1]
|
||||||
|
%proc.call if __content_filename__ == __caller_filename__
|
||||||
if __content_filename__ == __caller_filename__
|
|
||||||
%old_content_io, content_io = content_io, IO::Memory.new
|
|
||||||
%proc.call
|
|
||||||
%result = content_io.to_s
|
|
||||||
content_io = %old_content_io
|
|
||||||
%result
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,12 +60,10 @@ end
|
||||||
# ```
|
# ```
|
||||||
macro render(filename, layout)
|
macro render(filename, layout)
|
||||||
__content_filename__ = {{filename}}
|
__content_filename__ = {{filename}}
|
||||||
content_io = IO::Memory.new
|
io = IO::Memory.new
|
||||||
ECR.embed {{filename}}, content_io
|
content = ECR.embed {{filename}}, io
|
||||||
content = content_io.to_s
|
ECR.embed {{layout}}, io
|
||||||
layout_io = IO::Memory.new
|
io.to_s
|
||||||
ECR.embed {{layout}}, layout_io
|
|
||||||
layout_io.to_s
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Render view with the given filename.
|
# Render view with the given filename.
|
||||||
|
|
|
@ -36,7 +36,7 @@ module Kemal
|
||||||
end
|
end
|
||||||
|
|
||||||
file_path = File.join(@public_dir, expanded_path)
|
file_path = File.join(@public_dir, expanded_path)
|
||||||
is_dir = Dir.exists?(file_path)
|
is_dir = Dir.exists? file_path
|
||||||
|
|
||||||
if request_path != expanded_path
|
if request_path != expanded_path
|
||||||
redirect_to context, expanded_path
|
redirect_to context, expanded_path
|
||||||
|
@ -44,19 +44,8 @@ module Kemal
|
||||||
redirect_to context, expanded_path + '/'
|
redirect_to context, expanded_path + '/'
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_dir
|
if Dir.exists?(file_path)
|
||||||
if config.is_a?(Hash) && config.fetch("dir_index", false) && File.exists?(File.join(file_path, "index.html"))
|
if config.is_a?(Hash) && config["dir_listing"] == true
|
||||||
file_path = File.join(@public_dir, expanded_path, "index.html")
|
|
||||||
|
|
||||||
last_modified = modification_time(file_path)
|
|
||||||
add_cache_headers(context.response.headers, last_modified)
|
|
||||||
|
|
||||||
if cache_request?(context, last_modified)
|
|
||||||
context.response.status_code = 304
|
|
||||||
return
|
|
||||||
end
|
|
||||||
send_file(context, file_path)
|
|
||||||
elsif config.is_a?(Hash) && config.fetch("dir_listing", false)
|
|
||||||
context.response.content_type = "text/html"
|
context.response.content_type = "text/html"
|
||||||
directory_listing(context.response, request_path, file_path)
|
directory_listing(context.response, request_path, file_path)
|
||||||
else
|
else
|
||||||
|
@ -75,9 +64,5 @@ module Kemal
|
||||||
call_next(context)
|
call_next(context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def modification_time(file_path)
|
|
||||||
File.info(file_path).modification_time
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue