From: Jean Privat Date: Fri, 27 May 2016 18:47:56 +0000 (-0400) Subject: Merge: Docunit --no-color X-Git-Url: http://nitlanguage.org?hp=0de4243adb8a3e5adecc07bb9bfe6523cf9d3b4a Merge: Docunit --no-color Small last nitpicks on nitnuit * --no-color mode * better summary at the end * working dir no more hidden and removed on success Pull-Request: #2131 --- diff --git a/lib/core/file.nit b/lib/core/file.nit index def1cd2..7f1d481 100644 --- a/lib/core/file.nit +++ b/lib/core/file.nit @@ -663,6 +663,19 @@ class Path return res end + # Is `self` the path to an existing directory ? + # + # ~~~nit + # assert ".".to_path.is_dir + # assert not "/etc/issue".to_path.is_dir + # assert not "/should/not/exist".to_path.is_dir + # ~~~ + fun is_dir: Bool do + var st = stat + if st == null then return false + return st.is_dir + end + # Delete a directory and all of its content # # Does not go through symbolic links and may get stuck in a cycle if there diff --git a/lib/nitcorn/file_server.nit b/lib/nitcorn/file_server.nit index 5995558..a9ef5f6 100644 --- a/lib/nitcorn/file_server.nit +++ b/lib/nitcorn/file_server.nit @@ -74,6 +74,12 @@ class FileServer # Show directory listing? var show_directory_listing = true is writable + # Default file returned when no static file matches the requested URI. + # + # If no `default_file` is provided, the FileServer responds 404 error to + # unmatched queries. + var default_file: nullable String = null is writable + redef fun answer(request, turi) do var response @@ -86,15 +92,14 @@ class FileServer # This make sure that the requested file is within the root folder. if (local_file + "/").has_prefix(root) then # Does it exists? - if local_file.file_exists then - if local_file.file_stat.is_dir then + var file_stat = local_file.file_stat + if file_stat != null then + if file_stat.is_dir then # If we target a directory without an ending `/`, # redirect to the directory ending with `/`. - if not request.uri.is_empty and - request.uri.chars.last != '/' then - response = new HttpResponse(303) - response.header["Location"] = request.uri + "/" - return response + var uri = request.uri + if not uri.is_empty and uri.chars.last != '/' then + return answer_redirection(request.uri + "/") end # Show index file instead of the directory listing @@ -108,34 +113,94 @@ class FileServer end end - var is_dir = local_file.file_stat.is_dir - if show_directory_listing and is_dir then - # Show the directory listing - var title = turi - var files = local_file.files + file_stat = local_file.file_stat + if show_directory_listing and file_stat != null and file_stat.is_dir then + response = answer_directory_listing(request, turi, local_file) + else if file_stat != null and not file_stat.is_dir then # It's a single file + response = answer_file(local_file) + else response = answer_default + else response = answer_default + else response = new HttpResponse(403) - alpha_comparator.sort files + if response.status_code != 200 then + var tmpl = error_page(response.status_code) + if header != null and tmpl isa ErrorTemplate then tmpl.header = header + response.body = tmpl.to_s + end - var links = new Array[String] - if turi.length > 1 then - var path = (request.uri + "/..").simplify_path - links.add ".." - end - for file in files do - var local_path = local_file.join_path(file).simplify_path - var web_path = file.simplify_path - if local_path.file_stat.is_dir then web_path = web_path + "/" - links.add "{file}" - end + return response + end + + # Answer the `default_file` if any. + fun answer_default: HttpResponse do + var default_file = self.default_file + if default_file == null then + return new HttpResponse(404) + end + + var local_file = (root / default_file).simplify_path + return answer_file(local_file) + end - var header = self.header - var header_code - if header != null then - header_code = header.write_to_string - else header_code = "" + # Answer a 303 redirection to `location`. + fun answer_redirection(location: String): HttpResponse do + var response = new HttpResponse(303) + response.header["Location"] = location + return response + end + + # Build a reponse containing a single `local_file`. + # + # Returns a 404 error if local_file does not exists. + fun answer_file(local_file: String): HttpResponse do + if not local_file.file_exists then return new HttpResponse(404) + + var response = new HttpResponse(200) + response.files.add local_file + + # Set Content-Type depending on the file extension + var ext = local_file.file_extension + if ext != null then + var media_type = media_types[ext] + if media_type != null then + response.header["Content-Type"] = media_type + else response.header["Content-Type"] = "application/octet-stream" + end - response = new HttpResponse(200) - response.body = """ + # Cache control + response.header["cache-control"] = cache_control + return response + end + + # Answer with a directory listing for files within `local_files`. + fun answer_directory_listing(request: HttpRequest, turi, local_file: String): HttpResponse do + # Show the directory listing + var title = turi + var files = local_file.files + + alpha_comparator.sort files + + var links = new Array[String] + if turi.length > 1 then + var path = (request.uri + "/..").simplify_path + links.add ".." + end + for file in files do + var local_path = local_file.join_path(file).simplify_path + var web_path = file.simplify_path + var file_stat = local_path.file_stat + if file_stat != null and file_stat.is_dir then web_path = web_path + "/" + links.add "{file}" + end + + var header = self.header + var header_code + if header != null then + header_code = header.write_to_string + else header_code = "" + + var response = new HttpResponse(200) + response.body = """ @@ -157,33 +222,7 @@ class FileServer """ - response.header["Content-Type"] = media_types["html"].as(not null) - else if not is_dir then - # It's a single file - response = new HttpResponse(200) - response.files.add local_file - - var ext = local_file.file_extension - if ext != null then - var media_type = media_types[ext] - if media_type != null then - response.header["Content-Type"] = media_type - else response.header["Content-Type"] = "application/octet-stream" - end - - # Cache control - response.header["cache-control"] = cache_control - - else response = new HttpResponse(404) - else response = new HttpResponse(404) - else response = new HttpResponse(403) - - if response.status_code != 200 then - var tmpl = error_page(response.status_code) - if header != null and tmpl isa ErrorTemplate then tmpl.header = header - response.body = tmpl.to_s - end - + response.header["Content-Type"] = media_types["html"].as(not null) return response end end diff --git a/lib/popcorn/README.md b/lib/popcorn/README.md index a136621..2bb6646 100644 --- a/lib/popcorn/README.md +++ b/lib/popcorn/README.md @@ -175,6 +175,17 @@ directory from where you launch your app. If you run the app from another directory, it’s safer to use the absolute path of the directory that you want to serve. +In some cases, you can want to redirect request to static files to a default file +instead of returning a 404 error. +This can be achieved by specifying a default file in the StaticHandler: + +~~~ +app.use("/static/", new StaticHandler("public/", "default.html")) +~~~ + +This way all non-matched queries to the StaticHandler will be answered with the +`default.html` file. + ## Advanced Routing **Routing** refers to the definition of application end points (URIs) and how @@ -832,8 +843,13 @@ to your angular controller: import popcorn var app = new App -app.use("/*", new StaticHandler("my-ng-app/")) +app.use("/*", new StaticHandler("my-ng-app/", "index.html")) app.listen("localhost", 3000) ~~~ +Because the StaticHandler will not find the angular routes as static files, +you must specify the path to the default angular controller. +In this example, the StaticHandler will redirect any unknown requests to the `index.html` +angular controller. + See the examples for a more detailed use case working with a JSON API. diff --git a/lib/popcorn/examples/angular/example_angular.nit b/lib/popcorn/examples/angular/example_angular.nit index ba7f4f2..9212a8e 100644 --- a/lib/popcorn/examples/angular/example_angular.nit +++ b/lib/popcorn/examples/angular/example_angular.nit @@ -40,5 +40,5 @@ end var app = new App app.use("/counter", new CounterAPI) -app.use("/*", new StaticHandler("www/")) +app.use("/*", new StaticHandler("www/", "index.html")) app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/static_files/example_static_default.nit b/lib/popcorn/examples/static_files/example_static_default.nit new file mode 100644 index 0000000..6aa5b34 --- /dev/null +++ b/lib/popcorn/examples/static_files/example_static_default.nit @@ -0,0 +1,21 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import popcorn + +var app = new App +app.use("/", new StaticHandler("public/", "default.html")) +app.listen("localhost", 3000) diff --git a/lib/popcorn/examples/static_files/public/default.html b/lib/popcorn/examples/static_files/public/default.html new file mode 100644 index 0000000..2abb789 --- /dev/null +++ b/lib/popcorn/examples/static_files/public/default.html @@ -0,0 +1,13 @@ + + + + + + Some Popcorn love + + + + +

Default Page

+ + diff --git a/lib/popcorn/pop_handlers.nit b/lib/popcorn/pop_handlers.nit index 1c20b67..5d84961 100644 --- a/lib/popcorn/pop_handlers.nit +++ b/lib/popcorn/pop_handlers.nit @@ -235,10 +235,16 @@ class StaticHandler # Static files directory to serve. var static_dir: String + # Default file to serve if nothing matches the request. + # + # `null` for no default file. + var default_file: nullable String + # Internal file server used to lookup and render files. var file_server: FileServer is lazy do var srv = new FileServer(static_dir) srv.show_directory_listing = false + srv.default_file = default_file return srv end diff --git a/lib/popcorn/tests/res/test_example_angular.res b/lib/popcorn/tests/res/test_example_angular.res index c07813f..bb94961 100644 --- a/lib/popcorn/tests/res/test_example_angular.res +++ b/lib/popcorn/tests/res/test_example_angular.res @@ -4,4 +4,19 @@ [Client] curl -s localhost:*****/counter -X POST {"label":"Visitors","value":1} [Client] curl -s localhost:*****/counter -{"label":"Visitors","value":1} \ No newline at end of file +{"label":"Visitors","value":1} +[Client] curl -s localhost:*****/not_found + + + + + ng-example + + +
+ + + + + + diff --git a/lib/popcorn/tests/res/test_example_static_default.res b/lib/popcorn/tests/res/test_example_static_default.res new file mode 100644 index 0000000..59392f6 --- /dev/null +++ b/lib/popcorn/tests/res/test_example_static_default.res @@ -0,0 +1,88 @@ + +[Client] curl -s localhost:*****/css/style.css +body { + color: blue; + padding: 20px; +} + +[Client] curl -s localhost:*****/js/app.js +alert("Hello World!"); + +[Client] curl -s localhost:*****/hello.html + + + + + + Some Popcorn love + + + + +

Hello Popcorn!

+ + maybe it's a kitten? + + + + + +[Client] curl -s localhost:*****/ + + + + + + Some Popcorn love + + + + +

Default Page

+ + + +[Client] curl -s localhost:*****/css/not_found.nit + + + + + + Some Popcorn love + + + + +

Default Page

+ + + +[Client] curl -s localhost:*****/static/css/not_found.nit + + + + + + Some Popcorn love + + + + +

Default Page

+ + + +[Client] curl -s localhost:*****/not_found.nit + + + + + + Some Popcorn love + + + + +

Default Page

+ + diff --git a/lib/popcorn/tests/test_example_angular.nit b/lib/popcorn/tests/test_example_angular.nit index d1ab509..821aa32 100644 --- a/lib/popcorn/tests/test_example_angular.nit +++ b/lib/popcorn/tests/test_example_angular.nit @@ -24,13 +24,14 @@ class TestClient system "curl -s {host}:{port}/counter" system "curl -s {host}:{port}/counter -X POST" system "curl -s {host}:{port}/counter" + system "curl -s {host}:{port}/not_found" # handled by angular controller return null end end var app = new App app.use("/counter", new CounterAPI) -app.use("/*", new StaticHandler("www/")) +app.use("/*", new StaticHandler("../examples/angular/www/", "index.html")) var host = test_host var port = test_port diff --git a/lib/popcorn/tests/test_example_static_default.nit b/lib/popcorn/tests/test_example_static_default.nit new file mode 100644 index 0000000..ced04e2 --- /dev/null +++ b/lib/popcorn/tests/test_example_static_default.nit @@ -0,0 +1,52 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2016 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base_tests +import example_static_default + +class TestClient + super ClientThread + + redef fun main do + system "curl -s {host}:{port}/css/style.css" + system "curl -s {host}:{port}/js/app.js" + system "curl -s {host}:{port}/hello.html" + system "curl -s {host}:{port}/" + + system "curl -s {host}:{port}/css/not_found.nit" + system "curl -s {host}:{port}/static/css/not_found.nit" + system "curl -s {host}:{port}/not_found.nit" + + return null + end +end + +var app = new App +app.use("/", new StaticHandler("../examples/static_files/public/", "default.html")) + +var host = test_host +var port = test_port + +var server = new AppThread(host, port, app) +server.start +0.1.sleep + +var client = new TestClient(host, port) +client.start +client.join +0.1.sleep + +exit 0 diff --git a/lib/readline.ini b/lib/readline.ini new file mode 100644 index 0000000..c7e2ee9 --- /dev/null +++ b/lib/readline.ini @@ -0,0 +1,11 @@ +[package] +name=readline +tags=lib +maintainer=Frédéric Vachon +license=Apache-2.0 +[upstream] +browse=https://github.com/nitlang/nit/tree/master/lib/readline.nit +git=https://github.com/nitlang/nit.git +git.directory=lib/readline.nit +homepage=http://nitlanguage.org +issues=https://github.com/nitlang/nit/issues diff --git a/lib/readline.nit b/lib/readline.nit new file mode 100644 index 0000000..783e4e4 --- /dev/null +++ b/lib/readline.nit @@ -0,0 +1,58 @@ +# This file is part of NIT (http://www.nitlanguage.org). +# +# Copyright 2016 Frédéric Vachon +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# GNU readline library wrapper +module readline is ldflags "-lreadline" + +in "C" `{ + #include + #include +`} + +private fun native_readline(prompt: NativeString): NativeString `{ + return readline(prompt); +`} + +private fun native_add_history(data: NativeString) `{ + if (data == NULL) return; + add_history(data); +`} + +# Set emacs keybindings mode +fun set_vi_mode `{ rl_editing_mode = 0; `} + +# Set emacs keybindings mode +fun set_emacs_mode `{ rl_editing_mode = 1; `} + +# Use the GNU Library readline function +# Returns `null` if EOF is read +# If `with_history` is true, it will save all commands in the history except +# empty strings and white characters strings +fun readline(message: String, with_history: nullable Bool): nullable String do + var line = native_readline(message.to_cstring) + if line.address_is_null then return null + + var nit_str = line.to_s + + if with_history != null and with_history then + if nit_str.trim != "" then native_add_history(line) + end + + return nit_str +end + +# Adds the data String to the history no matter what it contains +fun add_history(data: String) do native_add_history data.to_cstring diff --git a/tests/sav/test_readline.res b/tests/sav/test_readline.res new file mode 100644 index 0000000..666312a --- /dev/null +++ b/tests/sav/test_readline.res @@ -0,0 +1,9 @@ +prompt>line 1 +line 1 +prompt>line 2 +line 2 +prompt>line 2bis +line 2bis +prompt>line 3  ine   3  +line 3 +prompt> \ No newline at end of file diff --git a/tests/test_readline.inputs b/tests/test_readline.inputs new file mode 100644 index 0000000..59f6fe7 --- /dev/null +++ b/tests/test_readline.inputs @@ -0,0 +1,4 @@ +line 1 +line 2 +line 2bis +line 3 ine3 diff --git a/tests/test_readline.nit b/tests/test_readline.nit new file mode 100644 index 0000000..c254b24 --- /dev/null +++ b/tests/test_readline.nit @@ -0,0 +1,21 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import readline + +loop + var line = readline("prompt>", true) + if line == null then break + print line +end