diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3452e4a..f52233b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: sdogruyol patreon: sdogruyol open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b0f83c..3bc639b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# 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) - You can now set your own application name for startup message [#606](https://github.com/kemalcr/kemal/pull/606). Thanks @aravindavk :pray: diff --git a/shard.yml b/shard.yml index 72011fd..69a311a 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: kemal -version: 1.1.0 +version: 1.2.0 authors: - Serdar Dogruyol @@ -15,7 +15,7 @@ dependencies: development_dependencies: ameba: github: crystal-ameba/ameba - version: ~> 0.14.0 + version: ~> 1.0 crystal: ">= 0.36.0" diff --git a/spec/asset/hello_with_content_for.ecr b/spec/asset/hello_with_content_for.ecr index 149b294..b5460f9 100644 --- a/spec/asset/hello_with_content_for.ecr +++ b/spec/asset/hello_with_content_for.ecr @@ -1,5 +1,5 @@ Hello <%= name %> -<% content_for "custom" do %> -

Hello from otherside

+<% content_for "meta" do %> +Kemal Spec <% end %> \ No newline at end of file diff --git a/spec/asset/layout_with_yield.ecr b/spec/asset/layout_with_yield.ecr index f6cd673..3710c4a 100644 --- a/spec/asset/layout_with_yield.ecr +++ b/spec/asset/layout_with_yield.ecr @@ -1,6 +1,8 @@ + + <%= yield_content "meta" %> + <%= content %> - <%= yield_content "custom" %> \ No newline at end of file diff --git a/spec/asset/layout_with_yield_and_vars.ecr b/spec/asset/layout_with_yield_and_vars.ecr index 3a82a7a..d2a8a35 100644 --- a/spec/asset/layout_with_yield_and_vars.ecr +++ b/spec/asset/layout_with_yield_and_vars.ecr @@ -1,8 +1,10 @@ + + <%= yield_content "meta" %> + <%= content %> - <%= yield_content "custom" %> - <%= var1 %> - <%= var2 %> + <%= var1 %> + <%= var2 %> \ No newline at end of file diff --git a/spec/route_handler_spec.cr b/spec/route_handler_spec.cr index 93cf0f0..e3046e3 100644 --- a/spec/route_handler_spec.cr +++ b/spec/route_handler_spec.cr @@ -143,4 +143,40 @@ describe "Kemal::RouteHandler" do client_response.body.should eq("Redirecting to /login") client_response.headers.has_key?("Location").should eq(true) 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 diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 803aacf..0065848 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -4,8 +4,8 @@ require "../src/*" include Kemal class CustomLogHandler < Kemal::BaseLogHandler - def call(env) - call_next env + def call(context) + call_next(context) end def write(message) diff --git a/spec/static_file_handler_spec.cr b/spec/static_file_handler_spec.cr index e9307f6..309e10f 100644 --- a/spec/static_file_handler_spec.cr +++ b/spec/static_file_handler_spec.cr @@ -23,6 +23,15 @@ describe Kemal::StaticFileHandler do response.body.should eq(File.read("#{__DIR__}/static/dir/test.txt")) 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 response = handle HTTP::Request.new("GET", "/dir/test.txt") response.status_code.should eq(200) diff --git a/spec/view_spec.cr b/spec/view_spec.cr index 4705233..79b8768 100644 --- a/spec/view_spec.cr +++ b/spec/view_spec.cr @@ -22,7 +22,7 @@ describe "Views" do end request = HTTP::Request.new("GET", "/view/world") client_response = call_request_on_app(request) - client_response.body.should contain("Hello world") + client_response.body.strip.should eq("Hello world\n") end it "renders layout" do @@ -56,7 +56,17 @@ describe "Views" do end request = HTTP::Request.new("GET", "/view/world") client_response = call_request_on_app(request) - client_response.body.should contain("Hello world") - client_response.body.should contain("

Hello from otherside

") + client_response.body.scan("Hello world").size.should eq(1) + client_response.body.should contain("Kemal Spec") + 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("

Hello from otherside

") end end diff --git a/src/kemal.cr b/src/kemal.cr index 6f1a564..9222fe6 100644 --- a/src/kemal.cr +++ b/src/kemal.cr @@ -7,18 +7,18 @@ require "./kemal/helpers/*" module Kemal # Overload of `self.run` with the default startup logging. - def self.run(port : Int32?, args = ARGV) - self.run(port, args) { } + def self.run(port : Int32?, args = ARGV, trap_signal : Bool = true) + self.run(port, args, trap_signal) { } end # Overload of `self.run` without port. - def self.run(args = ARGV) - self.run(nil, args: args) + def self.run(args = ARGV, trap_signal : Bool = true) + self.run(nil, args: args, trap_signal: trap_signal) end # Overload of `self.run` to allow just a block. def self.run(args = ARGV, &block) - self.run(nil, args: args, &block) + self.run(nil, args: args, trap_signal: true, &block) end # The command to run a `Kemal` application. @@ -27,7 +27,7 @@ module Kemal # # To use custom command line arguments, set args to nil # - def self.run(port : Int32? = nil, args = ARGV, &block) + def self.run(port : Int32? = nil, args = ARGV, trap_signal : Bool = true, &block) Kemal::CLI.new args config = Kemal.config config.setup @@ -36,7 +36,7 @@ module Kemal # Test environment doesn't need to have signal trap and logging. if config.env != "test" setup_404 - setup_trap_signal + setup_trap_signal if trap_signal end server = config.server ||= HTTP::Server.new(config.handlers) diff --git a/src/kemal/config.cr b/src/kemal/config.cr index 4e62d8d..23372dd 100644 --- a/src/kemal/config.cr +++ b/src/kemal/config.cr @@ -31,7 +31,7 @@ module Kemal @host_binding = "0.0.0.0" @port = 3000 @env = ENV["KEMAL_ENV"]? || "development" - @serve_static = {"dir_listing" => false, "gzip" => true} + @serve_static = {"dir_listing" => false, "gzip" => true, "dir_index" => false} @public_folder = "./public" @logging = true @logger = nil diff --git a/src/kemal/ext/context.cr b/src/kemal/ext/context.cr index c2e51c6..70787b6 100644 --- a/src/kemal/ext/context.cr +++ b/src/kemal/ext/context.cr @@ -17,10 +17,11 @@ class HTTP::Server @params ||= Kemal::ParamParser.new(@request, route_lookup.params) end - def redirect(url : String, status_code : Int32 = 302, *, body : String? = nil) + def redirect(url : String, status_code : Int32 = 302, *, body : String? = nil, close : Bool = true) @response.headers.add "Location", url @response.status_code = status_code @response.print(body) if body + @response.close if close end def route diff --git a/src/kemal/ext/response.cr b/src/kemal/ext/response.cr index 1d11dd1..3645bd5 100644 --- a/src/kemal/ext/response.cr +++ b/src/kemal/ext/response.cr @@ -1,3 +1,7 @@ +# 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 Output def close diff --git a/src/kemal/handler.cr b/src/kemal/handler.cr index bb59238..6016ba7 100644 --- a/src/kemal/handler.cr +++ b/src/kemal/handler.cr @@ -27,8 +27,8 @@ module Kemal end end - def call(env : HTTP::Server::Context) - call_next(env) + def call(context : HTTP::Server::Context) + call_next(context) end # Processes the path based on `only` paths which is a `Array(String)`. diff --git a/src/kemal/helpers/helpers.cr b/src/kemal/helpers/helpers.cr index bf212a2..11a6b62 100644 --- a/src/kemal/helpers/helpers.cr +++ b/src/kemal/helpers/helpers.cr @@ -216,20 +216,7 @@ private def multipart(file, env : HTTP::Server::Context) env.response.headers["Accept-Ranges"] = "bytes" env.response.headers["Content-Range"] = "bytes #{startb}-#{endb}/#{fileb}" # MUST - 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 - + file.seek(startb) IO.copy(file, env.response, content_length) else env.response.content_length = fileb diff --git a/src/kemal/helpers/macros.cr b/src/kemal/helpers/macros.cr index 41c8793..71ba2be 100644 --- a/src/kemal/helpers/macros.cr +++ b/src/kemal/helpers/macros.cr @@ -1,4 +1,4 @@ -CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(String))).new +CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(Nil))).new # `content_for` is a set of helpers that allows you to capture # blocks inside views to be rendered later during the request. The most @@ -34,13 +34,7 @@ CONTENT_FOR_BLOCKS = Hash(String, Tuple(String, Proc(String))).new # layout, inside the tag, and each view can call `content_for` # setting the appropriate set of tags that should be added to the layout. macro content_for(key, file = __FILE__) - %proc = ->() { - __view_io__ = IO::Memory.new - {{ yield }} - __view_io__.to_s - } - - CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc + CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, ->() { {{ yield }} } nil end @@ -49,7 +43,14 @@ macro yield_content(key) if CONTENT_FOR_BLOCKS.has_key?({{key}}) __caller_filename__ = CONTENT_FOR_BLOCKS[{{key}}][0] %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 @@ -60,10 +61,12 @@ end # ``` macro render(filename, layout) __content_filename__ = {{filename}} - io = IO::Memory.new - content = ECR.embed {{filename}}, io - ECR.embed {{layout}}, io - io.to_s + content_io = IO::Memory.new + ECR.embed {{filename}}, content_io + content = content_io.to_s + layout_io = IO::Memory.new + ECR.embed {{layout}}, layout_io + layout_io.to_s end # Render view with the given filename. diff --git a/src/kemal/static_file_handler.cr b/src/kemal/static_file_handler.cr index 109e971..dbeca28 100644 --- a/src/kemal/static_file_handler.cr +++ b/src/kemal/static_file_handler.cr @@ -36,7 +36,7 @@ module Kemal end 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 redirect_to context, expanded_path @@ -44,8 +44,19 @@ module Kemal redirect_to context, expanded_path + '/' end - if Dir.exists?(file_path) - if config.is_a?(Hash) && config["dir_listing"] == true + if is_dir + if config.is_a?(Hash) && config.fetch("dir_index", false) && File.exists?(File.join(file_path, "index.html")) + 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" directory_listing(context.response, request_path, file_path) else @@ -64,5 +75,9 @@ module Kemal call_next(context) end end + + private def modification_time(file_path) + File.info(file_path).modification_time + end end end