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