twitter/spitball

View on GitHub
bin/spitball-server

Summary

Maintainability
Test Coverage
#!/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