├── bin └── .gitkeep ├── spec ├── spec_helper.cr └── serve_spec.cr ├── src ├── serve │ ├── version.cr │ ├── bootstrap.cr │ ├── completion.cr │ └── cli.cr └── serve.cr ├── .gitignore ├── README └── screenshot.png ├── .travis.yml ├── shard.yml ├── CHANGELOG.md ├── Makefile ├── LICENSE └── README.md /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/serve" 3 | -------------------------------------------------------------------------------- /src/serve/version.cr: -------------------------------------------------------------------------------- 1 | module Serve 2 | VERSION = "0.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /src/serve/bootstrap.cr: -------------------------------------------------------------------------------- 1 | require "../serve" 2 | 3 | Serve::CLI.run(ARGV) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /lib/ 4 | /.crystal/ 5 | /.shards/ 6 | /bin/serve 7 | 8 | -------------------------------------------------------------------------------- /README/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperPaintman/serve/HEAD/README/screenshot.png -------------------------------------------------------------------------------- /spec/serve_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Serve do 4 | # TODO: Write tests 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | 3 | script: crystal spec --verbose 4 | 5 | branches: 6 | except: 7 | - gh-pages 8 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: serve 2 | version: 0.1.1 3 | 4 | authors: 5 | - SuperPaintman 6 | 7 | idescription: | 8 | Command line static HTTP server 9 | 10 | license: MIT 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.1 2 | 3 | - [**Added**] custom script for **Travis CI**. 4 | - [**Added**] `Makefile`. 5 | - [**Fixed**] auto-runin server in tests. 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | 10 | # 0.1.0 11 | 12 | - [**Added**] cli HTTP static server. 13 | - [**Added**] zsh tab completion code generator. 14 | - [**Added**] meta files. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CRYSTAL_BIN ?= $(shell which crystal) 2 | SERVE_BIN ?= $(shell which serve) 3 | PREFIX ?= /usr/local 4 | 5 | all: clean build 6 | 7 | build: 8 | $(CRYSTAL_BIN) deps 9 | $(CRYSTAL_BIN) build --release -o bin/serve src/serve/bootstrap.cr $(CRFLAGS) 10 | 11 | clean: 12 | rm -f ./bin/serve 13 | 14 | test: 15 | $(CRYSTAL_BIN) spec --verbose 16 | 17 | spec: test 18 | 19 | install: build 20 | mkdir -p $(PREFIX)/bin 21 | cp ./bin/serve $(PREFIX)/bin 22 | 23 | reinstall: build 24 | cp -rf ./bin/serve $(SERVE_BIN) 25 | 26 | .PHONY: all build clean test spec install reinstall 27 | -------------------------------------------------------------------------------- /src/serve/completion.cr: -------------------------------------------------------------------------------- 1 | module Serve 2 | module Completion 3 | def self.zsh : String 4 | <<-EOF 5 | #!/bin/zsh 6 | 7 | # if [[ -z $commands[serve] ]]; then 8 | # echo 'serve is not installed, you should install it first' 9 | # return -1 10 | # fi 11 | 12 | # Serve 13 | # version: #{Serve::VERSION} 14 | 15 | # Usage: 16 | # eval "$(serve --completion=zsh)" 17 | 18 | function _completion_serve() { 19 | local ret=1 20 | 21 | _arguments -C \\ 22 | '(-H, --host)'{-H,--host=}'[Server host]:host: ' \\ 23 | '(-p, --port)'{-p,--port=}'[Server port]:port: ' \\ 24 | '(--no-color)--no-color[Turn off colored log]' \\ 25 | '(--completion)--completion=[Print tab auto-completion for Serve]:shell:(#{Serve::SHELLS.join(" ")})' \\ 26 | '(-v, --version)'{-v,--version}'[Show serve version]' \\ 27 | '(-h, --help)'{-h,--help}'[Show this help]' \\ 28 | '*:directory:_directories' && ret=0 29 | 30 | return ret 31 | } 32 | 33 | compdef _completion_serve serve 34 | EOF 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexander Krivoshhekov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/serve/cli.cr: -------------------------------------------------------------------------------- 1 | require "option_parser" 2 | 3 | # Completions 4 | module Serve 5 | class CLI 6 | def self.run(args) 7 | host = "0.0.0.0" 8 | port = 8000 9 | public_dir = "./" 10 | colors = true 11 | 12 | OptionParser.parse(args) do |parser| 13 | parser.banner = "Usage: serve [arguments] [public_dir]" 14 | parser.on("-H HOST", "--host=HOST", "Server host") do |val| 15 | host = val 16 | end 17 | parser.on("-p PORT", "--port=PORT", "Server port") do |val| 18 | port = val.to_i 19 | end 20 | parser.on("--no-color", "Turn off colored log") do 21 | colors = false 22 | end 23 | parser.on("--completion=SHELL", "Print tab auto-completion for Serve") do |val| 24 | {% for name, index in Serve::SHELLS %} 25 | if val == {{name}} 26 | puts Serve::Completion.{{name.id}} 27 | exit 28 | end 29 | {% end %} 30 | 31 | raise "Unknown shell: #{val.downcase}. Available: #{Serve::SHELLS.join(", ")}" 32 | end 33 | parser.on("-v", "--version", "Show serve version") do 34 | puts Serve::VERSION 35 | exit 36 | end 37 | parser.on("-h", "--help", "Show this help") do 38 | puts parser 39 | exit 40 | end 41 | 42 | parser.unknown_args do |before_dash, after_dash| 43 | if before_dash.size > 1 44 | puts "Invalid args: #{before_dash[1..before_dash.size].join(", ")}" 45 | puts parser 46 | exit 1 47 | end 48 | 49 | if before_dash.empty? 50 | next 51 | end 52 | 53 | public_dir = before_dash[0] 54 | end 55 | end 56 | 57 | Serve.run(host: host, port: port, public_dir: public_dir, colors: colors) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serve 2 | 3 | [![Linux Build][travis-image]][travis-url] 4 | [![Shards version][shards-image]][shards-url] 5 | 6 | 7 | Command line static HTTP server 8 | 9 | 10 | ![Screenshot][screenshot-image] 11 | 12 | 13 | ## Installation 14 | 15 | Download from **github**: 16 | 17 | ```sh 18 | $ serve_version="0.1.1" 19 | $ serve_arch="x86_64" 20 | $ curl -Lo /usr/local/bin/serve.gz "https://github.com/SuperPaintman/serve/releases/download/v${serve_version}/serve-${serve_version}_linux_${serve_arch}.gz" 21 | $ gunzip /usr/local/bin/serve.gz 22 | $ chmod +x /usr/local/bin/serve 23 | ``` 24 | 25 | 26 | From sources: 27 | 28 | ```sh 29 | $ cd ~/Projects 30 | $ git clone https://github.com/SuperPaintman/serve 31 | $ cd ./serve 32 | $ make 33 | $ sudo make install 34 | $ # or 35 | $ sudo make reinstall 36 | ``` 37 | 38 | 39 | -------------------------------------------------------------------------------- 40 | 41 | ## Usage 42 | 43 | ```sh 44 | $ serve -h 45 | ``` 46 | 47 | 48 | -------------------------------------------------------------------------------- 49 | 50 | ## Test 51 | 52 | ```sh 53 | $ crystal spec 54 | # or 55 | $ make test 56 | ``` 57 | 58 | 59 | -------------------------------------------------------------------------------- 60 | 61 | ## Shell tab auto-completion 62 | 63 | To enable tab auto-completion for **Serve**, add one of the following lines 64 | to your `~/.zshrc` file. 65 | 66 | ```sh 67 | # Zsh, ~/.zshrc 68 | if [[ -z $commands[serve] ]]; then 69 | echo 'serve is not installed, you should install it first' 70 | else 71 | eval "$(serve --completion=zsh)" 72 | fi 73 | ``` 74 | 75 | 76 | -------------------------------------------------------------------------------- 77 | 78 | ## Contributing 79 | 80 | 1. Fork it () 81 | 2. Create your feature branch (`git checkout -b feature/`) 82 | 3. Commit your changes (`git commit -am 'Added some feature'`) 83 | 4. Push to the branch (`git push origin feature/`) 84 | 5. Create a new Pull Request 85 | 86 | 87 | -------------------------------------------------------------------------------- 88 | 89 | ## Contributors 90 | 91 | - [SuperPaintman](https://github.com/SuperPaintman) SuperPaintman - creator, maintainer 92 | 93 | 94 | -------------------------------------------------------------------------------- 95 | 96 | ## API 97 | [Docs][docs-url] 98 | 99 | 100 | -------------------------------------------------------------------------------- 101 | 102 | ## Changelog 103 | [Changelog][changelog-url] 104 | 105 | 106 | -------------------------------------------------------------------------------- 107 | 108 | ## License 109 | 110 | [MIT][license-url] 111 | 112 | 113 | [license-url]: LICENSE 114 | [changelog-url]: CHANGELOG.md 115 | [docs-url]: https://superpaintman.github.io/serve/ 116 | [screenshot-image]: README/screenshot.png 117 | [travis-image]: https://img.shields.io/travis/SuperPaintman/serve/master.svg?label=linux 118 | [travis-url]: https://travis-ci.org/SuperPaintman/serve 119 | [shards-image]: https://img.shields.io/github/tag/superpaintman/serve.svg?label=shards 120 | [shards-url]: https://github.com/superpaintman/serve 121 | 122 | -------------------------------------------------------------------------------- /src/serve.cr: -------------------------------------------------------------------------------- 1 | require "http" 2 | require "colorize" 3 | 4 | require "./serve/version" 5 | require "./serve/cli" 6 | require "./serve/completion" 7 | 8 | module Serve 9 | SERVER_NAME = "Serve" 10 | 11 | SHELLS = ["zsh"] 12 | 13 | COLORS = { 14 | :time => :dark_gray, 15 | :method => :green, 16 | :resource => :cyan, 17 | :version => :magenta, 18 | :status_code => :blue, 19 | :elapsed_text => :yellow, 20 | } 21 | 22 | def self.run(*, host = "0.0.0.0", port = 8000, public_dir = "./", colors = true) 23 | handler = Serve::StaticFileHandler.new(public_dir) 24 | logger = Serve::LogPrettyHandler.new(STDOUT, colors: colors) 25 | 26 | server = HTTP::Server.new(host, port, [logger, handler]) 27 | public_dir = File.expand_path(public_dir) 28 | 29 | if colors 30 | host = host.colorize(:yellow) 31 | port = port.colorize(:yellow) 32 | public_dir = public_dir.colorize(:cyan) 33 | end 34 | 35 | puts "Serving #{public_dir} dir on #{host}:#{port}" 36 | 37 | server.listen 38 | end 39 | 40 | class StaticFileHandler < HTTP::StaticFileHandler 41 | def call(context) 42 | # Rewrite path 43 | if context.request.path.ends_with?("/") 44 | index_path = context.request.path + "index.html" 45 | index_realpath = @public_dir + index_path 46 | 47 | if File.exists?(index_realpath) 48 | context.response.status_code = 200 49 | context.request.path = index_path 50 | end 51 | end 52 | 53 | # Add server name 54 | context.response.headers["Server"] = Serve::SERVER_NAME 55 | 56 | super 57 | end 58 | end 59 | 60 | class LogPrettyHandler < HTTP::LogHandler 61 | def initialize(@io : IO = STDOUT, *, @colors = true) 62 | end 63 | 64 | def call(context) 65 | time = Time.now 66 | 67 | call_next(context) 68 | 69 | time_str = time.to_s 70 | 71 | elapsed = Time.now - time 72 | elapsed_text = elapsed_text(elapsed) 73 | 74 | # Headers 75 | # X-Response-Time 76 | millis = elapsed.total_milliseconds.to_s 77 | context.response.headers["X-Response-Time"] = millis + "ms" 78 | 79 | method = context.request.method 80 | resource = context.request.resource 81 | version = context.request.version 82 | status_code = context.response.status_code 83 | 84 | if @colors 85 | time_str = time_str.colorize(Serve::COLORS[:time]) 86 | elapsed_text = elapsed_text.colorize(Serve::COLORS[:elapsed_text]) 87 | 88 | method = method.colorize(Serve::COLORS[:method]) 89 | resource = resource.colorize(Serve::COLORS[:resource]) 90 | version = version.colorize(Serve::COLORS[:version]) 91 | status_code = status_code.colorize(Serve::COLORS[:status_code]) 92 | end 93 | 94 | # TODO: add ip addr 95 | # 127.0.0.1 - - [13/Nov/2016 12:24:51] "GET /src/ HTTP/1.1" 200 - 96 | 97 | @io.puts "[#{time_str}] #{method} #{resource} #{version} - #{status_code} (#{elapsed_text})" 98 | rescue e 99 | time = Time.now 100 | time_str = time.to_s 101 | 102 | method = context.request.method 103 | resource = context.request.resource 104 | version = context.request.version 105 | 106 | message = "Unhandled #exception" 107 | 108 | if @colors 109 | time_str = time_str.colorize(Serve::COLORS[:time]) 110 | 111 | method = method.colorize(Serve::COLORS[:method]) 112 | resource = resource.colorize(Serve::COLORS[:resource]) 113 | version = version.colorize(Serve::COLORS[:version]) 114 | 115 | message = message.colorize(:red) 116 | end 117 | 118 | @io.puts "[#{time_str}] #{method} #{resource} #{version} - #{message}:" 119 | e.inspect_with_backtrace(@io) 120 | raise e 121 | end 122 | end 123 | end 124 | --------------------------------------------------------------------------------