Serveit script
This commit is contained in:
parent
6757f62347
commit
9da1deec6d
1 changed files with 203 additions and 0 deletions
203
bin/serveit
Executable file
203
bin/serveit
Executable file
|
|
@ -0,0 +1,203 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require "find"
|
||||||
|
require "webrick"
|
||||||
|
require "open3"
|
||||||
|
require "optparse"
|
||||||
|
|
||||||
|
class ServeIt
|
||||||
|
VERSION = [0, 0, 1]
|
||||||
|
def self.main
|
||||||
|
serve_dir, ignored_paths, command = parse_opts
|
||||||
|
serve_dir = File.expand_path(serve_dir)
|
||||||
|
Server.new(serve_dir, ignored_paths, command).serve
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.parse_opts
|
||||||
|
options = {:serve_dir => ".",
|
||||||
|
:ignored_paths => []}
|
||||||
|
parser = OptionParser.new do |opts|
|
||||||
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options] command"
|
||||||
|
opts.on_tail("-s", "--serve-dir DIR", "Root directory for server") do |dir|
|
||||||
|
options[:serve_dir] = dir
|
||||||
|
end
|
||||||
|
opts.on_tail("-i", "--ignore PATH", "Ignore changes to file or directory") do |path|
|
||||||
|
options[:ignored_paths] << path
|
||||||
|
end
|
||||||
|
opts.on_tail("--version", "Show version") do |dir|
|
||||||
|
puts ServeIt::VERSION.join('.')
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
parser.parse!(ARGV)
|
||||||
|
rescue OptionParser::InvalidOption => e
|
||||||
|
$stderr.puts e
|
||||||
|
$stderr.puts parser
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if ARGV.count == 0
|
||||||
|
command = nil
|
||||||
|
elsif ARGV.count == 1
|
||||||
|
command = ARGV.fetch(0)
|
||||||
|
else
|
||||||
|
$stderr.write parser.to_s
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
[options.fetch(:serve_dir), options.fetch(:ignored_paths), command]
|
||||||
|
end
|
||||||
|
|
||||||
|
class Server
|
||||||
|
def initialize(serve_dir, ignored_paths, command)
|
||||||
|
@mutex = Mutex.new
|
||||||
|
@serve_dir = serve_dir
|
||||||
|
@command = command
|
||||||
|
@rebuilder = Rebuilder.new(@command, ignored_paths) if @command
|
||||||
|
end
|
||||||
|
|
||||||
|
def serve
|
||||||
|
port = 8000
|
||||||
|
puts "Starting server at http://localhost:#{port}"
|
||||||
|
server = WEBrick::HTTPServer.new(:Port => port)
|
||||||
|
|
||||||
|
server.mount_proc '/' do |req, res|
|
||||||
|
relative_path = req.path.sub(/^\//, '')
|
||||||
|
local_abs_path = File.absolute_path(relative_path, @serve_dir)
|
||||||
|
|
||||||
|
if relative_path == "favicon.ico"
|
||||||
|
respond_to_favicon(res)
|
||||||
|
else
|
||||||
|
respond_to_path(res, relative_path, local_abs_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trap 'INT' do server.shutdown end
|
||||||
|
server.start
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_favicon(res)
|
||||||
|
res.status = 404
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_path(res, relative_path, local_abs_path)
|
||||||
|
begin
|
||||||
|
rebuild_if_needed
|
||||||
|
rescue Rebuilder::RebuildFailed => e
|
||||||
|
return respond_to_error(res, e.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
if File.directory?(local_abs_path)
|
||||||
|
respond_to_dir(res, relative_path, local_abs_path)
|
||||||
|
else
|
||||||
|
# We're building a file
|
||||||
|
respond_to_file(res, local_abs_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_error(res, message)
|
||||||
|
res.content_type = "text/html"
|
||||||
|
res.body = "<pre>" + message + "</pre>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_dir(res, rel_path, local_abs_path)
|
||||||
|
res.content_type = "text/html"
|
||||||
|
res.body = (
|
||||||
|
"<p><h3>Listing for /#{rel_path}</h3></p>\n" +
|
||||||
|
Dir.entries(local_abs_path).select do |child|
|
||||||
|
child != "."
|
||||||
|
end.sort.map do |child|
|
||||||
|
full_child_path_on_server = File.join("/", rel_path, child)
|
||||||
|
%{<a href="#{full_child_path_on_server}">#{child}</a><br>}
|
||||||
|
end.join("\n")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_to_file(res, local_abs_path)
|
||||||
|
res.body = File.read(local_abs_path)
|
||||||
|
res.content_type = guess_content_type(local_abs_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def guess_content_type(path)
|
||||||
|
extension = File.extname(path).sub(/^\./, '')
|
||||||
|
WEBrick::HTTPUtils::DefaultMimeTypes.fetch(extension) do
|
||||||
|
"application/octet-stream"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_if_needed
|
||||||
|
# Webrick is multi-threaded; guard against concurrent builds
|
||||||
|
@mutex.synchronize do
|
||||||
|
if @rebuilder
|
||||||
|
@rebuilder.rebuild_if_needed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Rebuilder
|
||||||
|
def initialize(command, ignored_paths)
|
||||||
|
@command = command
|
||||||
|
@ignored_paths = ignored_paths
|
||||||
|
@last_disk_state = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild_if_needed
|
||||||
|
if disk_state != @last_disk_state
|
||||||
|
stdout_and_stderr, success = rebuild
|
||||||
|
if !success
|
||||||
|
message = "Failed to build! Command output:\n\n" + stdout_and_stderr
|
||||||
|
raise RebuildFailed.new(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get a new post-build disk state so we don't pick up changes made during
|
||||||
|
# the build.
|
||||||
|
@last_disk_state = disk_state
|
||||||
|
[stdout_and_stderr, success]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebuild
|
||||||
|
puts "Running command: #{@command}"
|
||||||
|
puts " begin build".rjust(80, "=")
|
||||||
|
start_time = Time.now
|
||||||
|
stdout_and_stderr, status = Open3.capture2e(@command)
|
||||||
|
print stdout_and_stderr
|
||||||
|
puts (" built in %.03fs" % (Time.now - start_time)).rjust(80, "=")
|
||||||
|
[stdout_and_stderr, status.success?]
|
||||||
|
end
|
||||||
|
|
||||||
|
def disk_state
|
||||||
|
start_time = Time.now
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
Find.find(".") do |path|
|
||||||
|
if ignore_path?(path)
|
||||||
|
Find.prune
|
||||||
|
else
|
||||||
|
paths << path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
paths.map do |path|
|
||||||
|
[path, File.stat(path).mtime.to_s]
|
||||||
|
end.sort.tap do
|
||||||
|
puts (" scanned in %.03fs" % (Time.now - start_time)).rjust(80, "=")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignore_path?(path)
|
||||||
|
@ignored_paths.any? do |ignored_path|
|
||||||
|
File.absolute_path(path) == File.absolute_path(ignored_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RebuildFailed < RuntimeError; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if __FILE__ == $PROGRAM_NAME
|
||||||
|
ServeIt.main
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue