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 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
+