spitball 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.sample ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gem 'mysql'
4
+ gem 'json'
5
+
6
+ gem 'activesupport'
7
+
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]
@@ -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,11 @@
1
+ require 'digest/sha1'
2
+
3
+ module Spitball::Digest
4
+ def digest
5
+ @digest ||= ::Digest::SHA1.hexdigest gemfile
6
+ end
7
+
8
+ def hash
9
+ digest.hash
10
+ end
11
+ 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
+