bin/spitball-server
#!/usr/bin/env ruby
require 'spitball'
require 'optparse'
require 'sinatra'
require 'json'
configure { set :server, :puma }
# cargo culting sinatra's option parser, since it has trouble
# with rubygem stub files
OptionParser.new { |op|
op.on('-x') { set :lock, true }
op.on('-e env') { |val| set :environment, val.to_sym }
op.on('-s server') { |val| set :server, val }
op.on('-p port') { |val| set :port, val.to_i }
op.on('-o addr') { |val| set :bind, val }
}.parse!(ARGV.dup)
# always run
set :run, true
mime_type :gemfile, 'text/plain'
mime_type :lock, 'text/plain'
mime_type :tgz, 'application/x-compressed'
# return json array of cached SHAs
get '/list' do
content_type :json
JSON.dump Spitball::Repo.cached_digests
end
get '/env' do
content_type :text
`#{$:.inspect}\n#{Spitball.gem_cmd} env`
end
# return tgz or gemfile of cached SHA or 404
get '/:digest.:format' do |digest, format|
error 400 unless ['tgz', 'gemfile'].include? format
# this returns 404 on Errno::ENOENT
send_file Spitball::Repo.bundle_path(digest, format), :type => format
end
class Streamer
def initialize(io); @io = io end
def each; while buf = @io.read(200); yield buf end end
end
# POST a gemfile. Returns 201 Created if the bundle already exists, or
# 202 Accepted if it does not. The body of the response is the URI for
# the tarball.
post '/create' do
request_version = request.env["HTTP_#{Spitball::PROTOCOL_HEADER.gsub(/-/, '_').upcase}"]
if request_version != Spitball::PROTOCOL_VERSION
status 403
"Received version #{request_version} but need version #{Spitball::PROTOCOL_VERSION}"
else
without = request_version = request.env["HTTP_#{Spitball::WITHOUT_HEADER.gsub(/-/, '_').upcase}"]
without &&= without.split(',')
ball = Spitball.new(params['gemfile'], params['gemfile_lock'], :without => without, :bundle_config => params['bundle_config'])
url = "#{request.url.split("/create").first}/#{ball.digest}.tgz"
response['Location'] = url
if ball.cached?
status 201
"Bundle tarball pre-cached.\n"
else
status 202
i,o = IO.pipe
o.sync = true
# fork twice. once so we can have a pid to detach from in order to clean up,
# twice in order to properly redirect stdout for underlying shell commands.
pid = fork do
baller = open("|-")
if baller == nil # child
$stdout.sync = true
begin
ball.cache!
rescue Object => e
puts "#{e.class}: #{e}", e.backtrace.map{|l| "\t#{l}" }
end
else
while buf = baller.read(200)
$stdout.print buf
o.print buf
end
end
exit!
end
o.close
Process.detach(pid)
Streamer.new(i)
end
end
end