spitball 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile.sample +7 -0
- data/bin/spitball +51 -0
- data/bin/spitball-server +40 -0
- data/lib/spitball.rb +80 -0
- data/lib/spitball/digest.rb +11 -0
- data/lib/spitball/file_lock.rb +20 -0
- data/lib/spitball/remote.rb +41 -0
- data/lib/spitball/repo.rb +28 -0
- metadata +76 -0
data/Gemfile.sample
ADDED
data/bin/spitball
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'spitball'
|
4
|
+
require 'optparse'
|
5
|
+
require 'net/http'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
args = {}
|
9
|
+
|
10
|
+
opts = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: spitball [options] GEMFILE ARCHIVE"
|
12
|
+
opts.separator ""
|
13
|
+
opts.separator "options:"
|
14
|
+
|
15
|
+
opts.on('-h', '--host HOST', 'Get the tarball from a remote spitball server') do |host|
|
16
|
+
args[:host] = host
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on('-p', '--port PORT', 'Specify the remote server port. Default 8080') do |port|
|
20
|
+
args[:port] = port
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('--version') do
|
24
|
+
puts Spitball::VERSION
|
25
|
+
exit 0
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.separator ""
|
29
|
+
opts.separator "environment variables:"
|
30
|
+
opts.separator "\tSPITBALL_CACHE\t\t Specifies the cache dir. Defaults to /tmp/spitball"
|
31
|
+
opts.separator ""
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.permute!(ARGV)
|
35
|
+
|
36
|
+
|
37
|
+
args[:gemfile] = ARGV[0]
|
38
|
+
args[:destination] = ARGV[1]
|
39
|
+
|
40
|
+
unless args[:gemfile] and args[:destination]
|
41
|
+
puts opts.help
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
gemfile = File.read(args[:gemfile])
|
46
|
+
|
47
|
+
ball = args[:host] ?
|
48
|
+
Spitball::Remote.new(gemfile, args[:host], (args[:port] || 8080).to_i) :
|
49
|
+
Spitball.new(gemfile)
|
50
|
+
|
51
|
+
ball.copy_to args[:destination]
|
data/bin/spitball-server
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'spitball'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
mime_type :gemfile, 'text/plain'
|
8
|
+
mime_type :tgz, 'application/x-compressed'
|
9
|
+
|
10
|
+
# return json array of cached SHAs
|
11
|
+
get '/list' do
|
12
|
+
content_type :json
|
13
|
+
JSON.dump Spitball::Repo.list_cached
|
14
|
+
end
|
15
|
+
|
16
|
+
# return tgz or gemfile of cached SHA or 404
|
17
|
+
get '/:digest.:format' do |digest, format|
|
18
|
+
error 400 unless ['tgz', 'gemfile'].include? format
|
19
|
+
|
20
|
+
# this returns 404 on Errno::ENOENT
|
21
|
+
send_file Spitball::Repo.path(digest, format), :type => format
|
22
|
+
end
|
23
|
+
|
24
|
+
# 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.
|
25
|
+
post '/create' do
|
26
|
+
p request.body.read
|
27
|
+
request.body.rewind
|
28
|
+
ball = Spitball.new(request.body.read)
|
29
|
+
url = "#{request.url.split("/create").first}/#{ball.digest}.tgz"
|
30
|
+
response['Location'] = url
|
31
|
+
|
32
|
+
if ball.cached?
|
33
|
+
status 201
|
34
|
+
else
|
35
|
+
status 202
|
36
|
+
fork { ball.cache! }
|
37
|
+
end
|
38
|
+
|
39
|
+
url
|
40
|
+
end
|
data/lib/spitball.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class Spitball
|
4
|
+
require 'spitball/digest'
|
5
|
+
require 'spitball/repo'
|
6
|
+
require 'spitball/file_lock'
|
7
|
+
require 'spitball/remote'
|
8
|
+
|
9
|
+
class SpitballServerFailure < StandardError; end
|
10
|
+
class SpitballClientFailure < StandardError; end
|
11
|
+
class BundleCreationFailure < StandardError; end
|
12
|
+
|
13
|
+
VERSION = '1.0'
|
14
|
+
|
15
|
+
include Spitball::Digest
|
16
|
+
include Spitball::FileLock
|
17
|
+
|
18
|
+
attr_reader :gemfile
|
19
|
+
|
20
|
+
def initialize(gemfile)
|
21
|
+
@gemfile = gemfile
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy_to(dest)
|
25
|
+
cache!
|
26
|
+
FileUtils.cp(tarball_path, dest)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cached?
|
30
|
+
File.exist? tarball_path
|
31
|
+
end
|
32
|
+
|
33
|
+
def cache!(sync = true)
|
34
|
+
Spitball::Repo.make_cache_dir
|
35
|
+
|
36
|
+
unless cached?
|
37
|
+
if acquire_lock bundle_path('lock')
|
38
|
+
begin
|
39
|
+
create_bundle
|
40
|
+
ensure
|
41
|
+
release_lock bundle_path('lock')
|
42
|
+
end
|
43
|
+
elsif sync
|
44
|
+
sleep 0.1 until cached?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_bundle
|
50
|
+
File.open(gemfile_path, 'w') {|f| f.write gemfile }
|
51
|
+
FileUtils.mkdir_p bundle_path
|
52
|
+
|
53
|
+
if system "bundle install #{bundle_path} --gemfile=#{gemfile_path} --disable-shared-gems > /dev/null"
|
54
|
+
FileUtils.rm_rf File.join(bundle_path, "cache")
|
55
|
+
|
56
|
+
system "tar czf #{tarball_path}.#{Process.pid} -C #{bundle_path} ."
|
57
|
+
system "mv #{tarball_path}.#{Process.pid} #{tarball_path}"
|
58
|
+
|
59
|
+
else
|
60
|
+
FileUtils.rm_rf gemfile_path
|
61
|
+
raise BundleCreationFailure, "Bundle build failure."
|
62
|
+
end
|
63
|
+
|
64
|
+
FileUtils.rm_rf bundle_path
|
65
|
+
end
|
66
|
+
|
67
|
+
# Paths
|
68
|
+
|
69
|
+
def bundle_path(extension = nil)
|
70
|
+
Repo.path(digest, extension)
|
71
|
+
end
|
72
|
+
|
73
|
+
def gemfile_path
|
74
|
+
Repo.path(digest, 'gemfile')
|
75
|
+
end
|
76
|
+
|
77
|
+
def tarball_path
|
78
|
+
Repo.path(digest, 'tgz')
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Spitball::FileLock
|
4
|
+
def acquire_lock(lock_path)
|
5
|
+
pre_lock_path = "#{lock_path}_pre_#{Process.pid}"
|
6
|
+
|
7
|
+
File.open(pre_lock_path, 'w') {|f| f.write Process.pid }
|
8
|
+
|
9
|
+
# is this atomic?
|
10
|
+
system "mv -n #{pre_lock_path} #{lock_path}"
|
11
|
+
File.read(lock_path).to_i == Process.pid
|
12
|
+
ensure
|
13
|
+
FileUtils.rm_f pre_lock_path
|
14
|
+
end
|
15
|
+
|
16
|
+
# seems silly to lock to release lock
|
17
|
+
def release_lock(lock_path)
|
18
|
+
FileUtils.rm_f lock_path if acquire_lock lock_path
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class Spitball::Remote
|
5
|
+
|
6
|
+
def initialize(gemfile, host, port)
|
7
|
+
@gemfile = gemfile
|
8
|
+
@host = host
|
9
|
+
@port = port
|
10
|
+
end
|
11
|
+
|
12
|
+
def copy_to(path)
|
13
|
+
File.open(path, 'w') do |f|
|
14
|
+
f.write get_tarball_data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_tarball_data
|
19
|
+
url = URI.parse("http://#{@host}:#{@port}/create")
|
20
|
+
res = Net::HTTP.start(url.host, url.port) do |http|
|
21
|
+
http.post(url.path, @gemfile)
|
22
|
+
end
|
23
|
+
|
24
|
+
case res.code
|
25
|
+
when '201' # Created
|
26
|
+
Net::HTTP.get(URI.parse(res['Location']))
|
27
|
+
when '202' # Accepted
|
28
|
+
loop do
|
29
|
+
sleep 2
|
30
|
+
try = Net::HTTP.get_response(URI.parse(res['Location']))
|
31
|
+
next if try.code != '200'
|
32
|
+
return try.body
|
33
|
+
end
|
34
|
+
else
|
35
|
+
raise SpitballServerFailure, "Expected 2xx response code. Got #{res.code}."
|
36
|
+
end
|
37
|
+
rescue URI::InvalidURIError => e
|
38
|
+
raise SpitballClientError, e.message
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spitball::Repo
|
2
|
+
extend self
|
3
|
+
|
4
|
+
WORKING_DIR = ENV['SPITBALL_CACHE'] || '/tmp/spitball'
|
5
|
+
|
6
|
+
def path(digest, extension = nil)
|
7
|
+
extension = ".#{extension}" unless extension.nil? or extension.empty?
|
8
|
+
File.join WORKING_DIR, "bundle_#{digest}#{extension}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def exist?(digest)
|
12
|
+
File.exist? path(digest, 'tgz')
|
13
|
+
end
|
14
|
+
|
15
|
+
def gemfile(digest)
|
16
|
+
File.read path(digest, 'gemfile')
|
17
|
+
end
|
18
|
+
|
19
|
+
def list_cached
|
20
|
+
Dir[File.join(WORKING_DIR, 'bundle_*.tgz')].map do |path|
|
21
|
+
path.match(/bundle_(.*?)\.tgz$/)[1]
|
22
|
+
end.compact.uniq.sort
|
23
|
+
end
|
24
|
+
|
25
|
+
def make_cache_dir
|
26
|
+
FileUtils.mkdir_p WORKING_DIR
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spitball
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matt Freels
|
14
|
+
- Brandon Mitchell
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-07-15 00:00:00 -07:00
|
20
|
+
default_executable:
|
21
|
+
dependencies: []
|
22
|
+
|
23
|
+
description: Use bundler to generate gem tarball packages.
|
24
|
+
email: freels@twitter.com
|
25
|
+
executables:
|
26
|
+
- spitball
|
27
|
+
- spitball-server
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- Gemfile.sample
|
34
|
+
- bin/spitball
|
35
|
+
- bin/spitball-server
|
36
|
+
- lib/spitball.rb
|
37
|
+
- lib/spitball/digest.rb
|
38
|
+
- lib/spitball/file_lock.rb
|
39
|
+
- lib/spitball/remote.rb
|
40
|
+
- lib/spitball/repo.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://twitter.com
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.7
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: get a bundle
|
75
|
+
test_files: []
|
76
|
+
|