geminabox-bootstrap 0.10.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +91 -0
- data/lib/geminabox.rb +271 -0
- data/lib/geminabox/disk_cache.rb +72 -0
- data/lib/geminabox/gem_version.rb +36 -0
- data/lib/geminabox/gem_version_collection.rb +63 -0
- data/lib/geminabox/incoming_gem.rb +65 -0
- data/lib/geminabox/indexer.rb +46 -0
- data/lib/geminabox/version.rb +1 -0
- data/lib/geminabox_client.rb +60 -0
- data/lib/hostess.rb +42 -0
- data/lib/rubygems/commands/inabox_command.rb +91 -0
- data/lib/rubygems_plugin.rb +2 -0
- data/public/favicon.ico +0 -0
- data/public/vendor/bootstrap/css/bootstrap-responsive.css +1279 -0
- data/public/vendor/bootstrap/css/bootstrap-responsive.min.css +18 -0
- data/public/vendor/bootstrap/css/bootstrap.css +7607 -0
- data/public/vendor/bootstrap/css/bootstrap.min.css +18 -0
- data/public/vendor/bootstrap/css/prettify.min.css +30 -0
- data/public/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/public/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
- data/public/vendor/bootstrap/js/bootstrap.js +3060 -0
- data/public/vendor/bootstrap/js/bootstrap.min.js +7 -0
- data/public/vendor/bootstrap/js/prettify.min.js +28 -0
- data/public/vendor/fontawesome/css/font-awesome-ie7.min.css +22 -0
- data/public/vendor/fontawesome/css/font-awesome.css +540 -0
- data/public/vendor/fontawesome/css/font-awesome.min.css +33 -0
- data/public/vendor/fontawesome/font/FontAwesome.otf +0 -0
- data/public/vendor/fontawesome/font/fontawesome-webfont.eot +0 -0
- data/public/vendor/fontawesome/font/fontawesome-webfont.svg +284 -0
- data/public/vendor/fontawesome/font/fontawesome-webfont.ttf +0 -0
- data/public/vendor/fontawesome/font/fontawesome-webfont.woff +0 -0
- data/public/vendor/github-icon.png +0 -0
- data/public/vendor/jquery-1.9.1.min.js +5 -0
- data/views/adding_gems_partial.erb +0 -0
- data/views/atom.erb +28 -0
- data/views/example_gemfile.erb +5 -0
- data/views/gem.erb +66 -0
- data/views/index.erb +35 -0
- data/views/layout.erb +47 -0
- data/views/upload.erb +36 -0
- metadata +237 -0
data/README.markdown
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Gem in a Box – Really simple rubygem hosting
|
2
|
+
[![Build Status](https://secure.travis-ci.org/cwninja/geminabox.png)](http://travis-ci.org/cwninja/geminabox)
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/geminabox.png)](http://badge.fury.io/rb/geminabox)
|
4
|
+
|
5
|
+
Geminabox lets you host your own gems, and push new gems to it just like with rubygems.org.
|
6
|
+
The bundler dependencies API is supported out of the box.
|
7
|
+
Authentication is left up to either the web server, or the Rack stack.
|
8
|
+
For basic auth, try [Rack::Auth](http://rack.rubyforge.org/doc/Rack/Auth/Basic.html).
|
9
|
+
|
10
|
+
![screen shot](http://i.imgur.com/hjm2PEc.png)
|
11
|
+
|
12
|
+
## System Requirements
|
13
|
+
|
14
|
+
Tested on Mac OS X 10.8.2
|
15
|
+
Ruby 1.9.3-392
|
16
|
+
|
17
|
+
Tests fail on Ruby 2.0.0-p0
|
18
|
+
|
19
|
+
## Server Setup
|
20
|
+
|
21
|
+
gem install geminabox
|
22
|
+
|
23
|
+
Create a config.ru as follows:
|
24
|
+
|
25
|
+
require "rubygems"
|
26
|
+
require "geminabox"
|
27
|
+
|
28
|
+
Geminabox.data = "/var/geminabox-data" # ... or wherever
|
29
|
+
run Geminabox
|
30
|
+
|
31
|
+
Start your gem server with 'rackup' to run WEBrick or hook up the config.ru as you normally would ([passenger][passenger], [thin][thin], [unicorn][unicorn], whatever floats your boat).
|
32
|
+
|
33
|
+
## Legacy RubyGems index
|
34
|
+
|
35
|
+
RubyGems supports generating indexes for the so called legacy versions (< 1.2), and since it is very rare to use such versions nowadays, it can be disabled, thus improving indexing times for large repositories. If it's safe for your application, you can disable support for these legacy versions by adding the following configuration to your config.ru file:
|
36
|
+
|
37
|
+
Geminabox.build_legacy = false
|
38
|
+
|
39
|
+
## Client Usage
|
40
|
+
|
41
|
+
gem install geminabox
|
42
|
+
|
43
|
+
gem inabox pkg/my-awesome-gem-1.0.gem
|
44
|
+
|
45
|
+
Configure Gem in a box (interactive prompt to specify where to upload to):
|
46
|
+
|
47
|
+
geminabox -c
|
48
|
+
|
49
|
+
Change the host to upload to:
|
50
|
+
|
51
|
+
geminabox -g HOST
|
52
|
+
|
53
|
+
Simples!
|
54
|
+
|
55
|
+
## Command Line Help
|
56
|
+
|
57
|
+
Usage: gem inabox GEM [options]
|
58
|
+
|
59
|
+
Options:
|
60
|
+
-c, --configure Configure GemInABox
|
61
|
+
-g, --host HOST Host to upload to.
|
62
|
+
-o, --overwrite Overwrite Gem.
|
63
|
+
|
64
|
+
|
65
|
+
Common Options:
|
66
|
+
-h, --help Get help on this command
|
67
|
+
-V, --[no-]verbose Set the verbose level of output
|
68
|
+
-q, --quiet Silence commands
|
69
|
+
--config-file FILE Use this config file instead of default
|
70
|
+
--backtrace Show stack backtrace on errors
|
71
|
+
--debug Turn on Ruby debugging
|
72
|
+
|
73
|
+
|
74
|
+
Arguments:
|
75
|
+
GEM built gem to push up
|
76
|
+
|
77
|
+
Summary:
|
78
|
+
Push a gem up to your GemInABox
|
79
|
+
|
80
|
+
Description:
|
81
|
+
Push a gem up to your GemInABox
|
82
|
+
|
83
|
+
## Licence
|
84
|
+
|
85
|
+
Fork it, mod it, choose it, use it, make it better. All under the [do what the fuck you want to + beer/pizza public license][WTFBPPL].
|
86
|
+
|
87
|
+
[WTFBPPL]: http://tomlea.co.uk/WTFBPPL.txt
|
88
|
+
[sinatra]: http://www.sinatrarb.com/
|
89
|
+
[passenger]: http://www.modrails.com/
|
90
|
+
[thin]: http://code.macournoyer.com/thin/
|
91
|
+
[unicorn]: http://unicorn.bogomips.org/
|
data/lib/geminabox.rb
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'builder'
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'rubygems/indexer'
|
6
|
+
require 'rubygems/package'
|
7
|
+
require 'hostess'
|
8
|
+
require 'geminabox/version'
|
9
|
+
require 'rss/atom'
|
10
|
+
require 'tempfile'
|
11
|
+
|
12
|
+
class Geminabox < Sinatra::Base
|
13
|
+
enable :static, :methodoverride
|
14
|
+
|
15
|
+
set :public_folder, File.join(File.dirname(__FILE__), *%w[.. public])
|
16
|
+
set :data, File.join(File.dirname(__FILE__), *%w[.. data])
|
17
|
+
set :build_legacy, false
|
18
|
+
set :incremental_updates, true
|
19
|
+
set :views, File.join(File.dirname(__FILE__), *%w[.. views])
|
20
|
+
set :allow_replace, false
|
21
|
+
set :gem_permissions, 0644
|
22
|
+
use Hostess
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def disallow_replace?
|
26
|
+
! allow_replace
|
27
|
+
end
|
28
|
+
|
29
|
+
def fixup_bundler_rubygems!
|
30
|
+
return if @post_reset_hook_applied
|
31
|
+
Gem.post_reset{ Gem::Specification.all = nil } if defined? Bundler and Gem.respond_to? :post_reset
|
32
|
+
@post_reset_hook_applied = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
autoload :GemVersionCollection, "geminabox/gem_version_collection"
|
37
|
+
autoload :GemVersion, "geminabox/gem_version"
|
38
|
+
autoload :DiskCache, "geminabox/disk_cache"
|
39
|
+
autoload :IncomingGem, "geminabox/incoming_gem"
|
40
|
+
|
41
|
+
before do
|
42
|
+
headers 'X-Powered-By' => "geminabox #{GeminaboxVersion}"
|
43
|
+
end
|
44
|
+
|
45
|
+
get '/' do
|
46
|
+
@gems = load_gems
|
47
|
+
@index_gems = index_gems(@gems)
|
48
|
+
erb :index
|
49
|
+
end
|
50
|
+
|
51
|
+
get '/atom.xml' do
|
52
|
+
@gems = load_gems
|
53
|
+
erb :atom, :layout => false
|
54
|
+
end
|
55
|
+
|
56
|
+
get '/api/v1/dependencies' do
|
57
|
+
query_gems = params[:gems].to_s.split(',')
|
58
|
+
deps = query_gems.inject([]){|memo, query_gem| memo + gem_dependencies(query_gem) }
|
59
|
+
Marshal.dump(deps)
|
60
|
+
end
|
61
|
+
|
62
|
+
get '/upload' do
|
63
|
+
erb :upload
|
64
|
+
end
|
65
|
+
|
66
|
+
get '/example-gemfile' do
|
67
|
+
content_type 'text/plain'
|
68
|
+
erb :example_gemfile, :layout => false
|
69
|
+
end
|
70
|
+
|
71
|
+
get '/reindex' do
|
72
|
+
reindex(:force_rebuild)
|
73
|
+
redirect url("/")
|
74
|
+
end
|
75
|
+
|
76
|
+
get '/gems/:gemname' do
|
77
|
+
gems = Hash[load_gems.by_name]
|
78
|
+
@gem = gems[params[:gemname]]
|
79
|
+
halt 404 unless @gem
|
80
|
+
erb :gem
|
81
|
+
end
|
82
|
+
|
83
|
+
delete '/gems/*.gem' do
|
84
|
+
File.delete file_path if File.exists? file_path
|
85
|
+
reindex(:force_rebuild)
|
86
|
+
redirect url("/")
|
87
|
+
end
|
88
|
+
|
89
|
+
post '/upload' do
|
90
|
+
unless params[:file] && params[:file][:filename] && (tmpfile = params[:file][:tempfile])
|
91
|
+
@error = "No file selected"
|
92
|
+
halt [400, erb(:upload)]
|
93
|
+
end
|
94
|
+
handle_incoming_gem(IncomingGem.new(tmpfile))
|
95
|
+
end
|
96
|
+
|
97
|
+
post '/api/v1/gems' do
|
98
|
+
begin
|
99
|
+
handle_incoming_gem(IncomingGem.new(request.body))
|
100
|
+
rescue Object => o
|
101
|
+
File.open "/tmp/debug.txt", "a" do |io|
|
102
|
+
io.puts o, o.backtrace
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def handle_incoming_gem(gem)
|
110
|
+
prepare_data_folders
|
111
|
+
error_response(400, "Cannot process gem") unless gem.valid?
|
112
|
+
handle_replacement(gem) unless params[:overwrite] == "true"
|
113
|
+
write_and_index(gem)
|
114
|
+
|
115
|
+
if api_request?
|
116
|
+
"Gem #{gem.name} received and indexed. <a href=\"#{url("/")}\">Click here to return</a>"
|
117
|
+
else
|
118
|
+
redirect url("/")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def api_request?
|
123
|
+
request.accept.first != "text/html"
|
124
|
+
end
|
125
|
+
|
126
|
+
def error_response(code, message)
|
127
|
+
halt [code, message] if api_request?
|
128
|
+
html = <<HTML
|
129
|
+
<html>
|
130
|
+
<head><title>Error - #{code}</title></head>
|
131
|
+
<body>
|
132
|
+
<h1>Error - #{code}</h1>
|
133
|
+
<p>#{message}</p>
|
134
|
+
</body>
|
135
|
+
</html>
|
136
|
+
HTML
|
137
|
+
halt [code, html]
|
138
|
+
end
|
139
|
+
|
140
|
+
def prepare_data_folders
|
141
|
+
if File.exists? Geminabox.data
|
142
|
+
error_response( 500, "Please ensure #{File.expand_path(Geminabox.data)} is a directory." ) unless File.directory? Geminabox.data
|
143
|
+
error_response( 500, "Please ensure #{File.expand_path(Geminabox.data)} is writable by the geminabox web server." ) unless File.writable? Geminabox.data
|
144
|
+
else
|
145
|
+
begin
|
146
|
+
FileUtils.mkdir_p(settings.data)
|
147
|
+
rescue Errno::EACCES, Errno::ENOENT, RuntimeError => e
|
148
|
+
error_response( 500, "Could not create #{File.expand_path(Geminabox.data)}.\n#{e}\n#{e.message}" )
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
FileUtils.mkdir_p(File.join(settings.data, "gems"))
|
153
|
+
end
|
154
|
+
|
155
|
+
def handle_replacement(gem)
|
156
|
+
if Geminabox.disallow_replace? and File.exist?(gem.dest_filename)
|
157
|
+
existing_file_digest = Digest::SHA1.file(gem.dest_filename).hexdigest
|
158
|
+
|
159
|
+
if existing_file_digest != gem.hexdigest
|
160
|
+
error_response(409, "Updating an existing gem is not permitted.\nYou should either delete the existing version, or change your version number.")
|
161
|
+
else
|
162
|
+
error_response(200, "Ignoring upload, you uploaded the same thing previously.\nPlease use -o to ovewrite.")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def write_and_index(gem)
|
168
|
+
tmpfile = gem.gem_data
|
169
|
+
atomic_write(gem.dest_filename) do |f|
|
170
|
+
while blk = tmpfile.read(65536)
|
171
|
+
f << blk
|
172
|
+
end
|
173
|
+
end
|
174
|
+
reindex
|
175
|
+
end
|
176
|
+
|
177
|
+
def reindex(force_rebuild = false)
|
178
|
+
Geminabox.fixup_bundler_rubygems!
|
179
|
+
force_rebuild = true unless settings.incremental_updates
|
180
|
+
if force_rebuild
|
181
|
+
indexer.generate_index
|
182
|
+
dependency_cache.flush
|
183
|
+
else
|
184
|
+
begin
|
185
|
+
require 'geminabox/indexer'
|
186
|
+
updated_gemspecs = Geminabox::Indexer.updated_gemspecs(indexer)
|
187
|
+
Geminabox::Indexer.patch_rubygems_update_index_pre_1_8_25(indexer)
|
188
|
+
indexer.update_index
|
189
|
+
updated_gemspecs.each { |gem| dependency_cache.flush_key(gem.name) }
|
190
|
+
rescue => e
|
191
|
+
puts "#{e.class}:#{e.message}"
|
192
|
+
puts e.backtrace.join("\n")
|
193
|
+
reindex(:force_rebuild)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def indexer
|
199
|
+
Gem::Indexer.new(settings.data, :build_legacy => settings.build_legacy)
|
200
|
+
end
|
201
|
+
|
202
|
+
def file_path
|
203
|
+
File.expand_path(File.join(settings.data, *request.path_info))
|
204
|
+
end
|
205
|
+
|
206
|
+
def dependency_cache
|
207
|
+
@dependency_cache ||= Geminabox::DiskCache.new(File.join(settings.data, "_cache"))
|
208
|
+
end
|
209
|
+
|
210
|
+
def all_gems
|
211
|
+
%w(specs prerelease_specs).map{ |specs_file_type|
|
212
|
+
specs_file_path = File.join(settings.data, "#{specs_file_type}.#{Gem.marshal_version}.gz")
|
213
|
+
if File.exists?(specs_file_path)
|
214
|
+
Marshal.load(Gem.gunzip(Gem.read_binary(specs_file_path)))
|
215
|
+
else
|
216
|
+
[]
|
217
|
+
end
|
218
|
+
}.inject(:|)
|
219
|
+
end
|
220
|
+
|
221
|
+
def load_gems
|
222
|
+
@loaded_gems ||= Geminabox::GemVersionCollection.new(all_gems)
|
223
|
+
end
|
224
|
+
|
225
|
+
def index_gems(gems)
|
226
|
+
Set.new(gems.map{|gem| gem.name[0..0].downcase})
|
227
|
+
end
|
228
|
+
|
229
|
+
# based on http://as.rubyonrails.org/classes/File.html
|
230
|
+
def atomic_write(file_name)
|
231
|
+
temp_dir = File.join(settings.data, "_temp")
|
232
|
+
FileUtils.mkdir_p(temp_dir)
|
233
|
+
temp_file = Tempfile.new("." + File.basename(file_name), temp_dir)
|
234
|
+
yield temp_file
|
235
|
+
temp_file.close
|
236
|
+
File.rename(temp_file.path, file_name)
|
237
|
+
File.chmod(settings.gem_permissions, file_name)
|
238
|
+
end
|
239
|
+
|
240
|
+
helpers do
|
241
|
+
def spec_for(gem_name, version)
|
242
|
+
spec_file = File.join(settings.data, "quick", "Marshal.#{Gem.marshal_version}", "#{gem_name}-#{version}.gemspec.rz")
|
243
|
+
Marshal.load(Gem.inflate(File.read(spec_file))) if File.exists? spec_file
|
244
|
+
end
|
245
|
+
|
246
|
+
# Return a list of versions of gem 'gem_name' with the dependencies of each version.
|
247
|
+
def gem_dependencies(gem_name)
|
248
|
+
dependency_cache.marshal_cache(gem_name) do
|
249
|
+
load_gems.
|
250
|
+
select { |gem| gem_name == gem.name }.
|
251
|
+
map { |gem| [gem, spec_for(gem.name, gem.number)] }.
|
252
|
+
reject { |(_, spec)| spec.nil? }.
|
253
|
+
map do |(gem, spec)|
|
254
|
+
{
|
255
|
+
:name => gem.name,
|
256
|
+
:number => gem.number.version,
|
257
|
+
:platform => gem.platform,
|
258
|
+
:dependencies => runtime_dependencies(spec)
|
259
|
+
}
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def runtime_dependencies(spec)
|
265
|
+
spec.
|
266
|
+
dependencies.
|
267
|
+
select { |dep| dep.type == :runtime }.
|
268
|
+
map { |dep| [dep.name, dep.requirement.to_s] }
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
class Geminabox::DiskCache
|
4
|
+
attr_reader :root_path
|
5
|
+
|
6
|
+
def initialize(root_path)
|
7
|
+
@root_path = root_path
|
8
|
+
ensure_dir_exists!
|
9
|
+
end
|
10
|
+
|
11
|
+
def flush_key(key)
|
12
|
+
path = path(key_hash(key))
|
13
|
+
FileUtils.rm_f(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def flush
|
17
|
+
FileUtils.rm_rf(root_path)
|
18
|
+
ensure_dir_exists!
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache(key)
|
22
|
+
key_hash = key_hash(key)
|
23
|
+
read(key_hash) || write(key_hash, yield)
|
24
|
+
end
|
25
|
+
|
26
|
+
def marshal_cache(key)
|
27
|
+
key_hash = key_hash(key)
|
28
|
+
marshal_read(key_hash) || marshal_write(key_hash, yield)
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def ensure_dir_exists!
|
34
|
+
FileUtils.mkdir_p(root_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def key_hash(key)
|
38
|
+
Digest::MD5.hexdigest(key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def path(key_hash)
|
42
|
+
File.join(root_path, key_hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def read(key_hash)
|
46
|
+
read_int(key_hash) { |path| File.read(path) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def marshal_read(key_hash)
|
50
|
+
read_int(key_hash) { |path| Marshal.load(File.open(path)) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_int(key_hash)
|
54
|
+
path = path(key_hash)
|
55
|
+
yield(path) if File.exists?(path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def write(key_hash, value)
|
59
|
+
write_int(key_hash) { |f| f << value }
|
60
|
+
value
|
61
|
+
end
|
62
|
+
|
63
|
+
def marshal_write(key_hash, value)
|
64
|
+
write_int(key_hash) { |f| Marshal.dump(value, f) }
|
65
|
+
value
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_int(key_hash)
|
69
|
+
File.open(path(key_hash), 'wb') { |f| yield(f) }
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Geminabox::GemVersion
|
2
|
+
attr_accessor :name, :number, :platform
|
3
|
+
|
4
|
+
def initialize(name, number, platform)
|
5
|
+
@name = name
|
6
|
+
@number = number
|
7
|
+
@platform = platform
|
8
|
+
end
|
9
|
+
|
10
|
+
def ruby?
|
11
|
+
!!(platform =~ /ruby/i)
|
12
|
+
end
|
13
|
+
|
14
|
+
def version
|
15
|
+
Gem::Version.create(number)
|
16
|
+
end
|
17
|
+
|
18
|
+
def <=>(other)
|
19
|
+
sort = other.name <=> name
|
20
|
+
sort = version <=> other.version if sort.zero?
|
21
|
+
sort = (other.ruby? && !ruby?) ? 1 : -1 if sort.zero? && ruby? != other.ruby?
|
22
|
+
sort = other.platform <=> platform if sort.zero?
|
23
|
+
|
24
|
+
sort
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
return false unless other.class == self.class
|
29
|
+
[name, number, platform] == [other.name, other.number, other.platform]
|
30
|
+
end
|
31
|
+
|
32
|
+
def gemfile_name
|
33
|
+
included_platform = ruby? ? nil : platform
|
34
|
+
[name, number, included_platform].compact.join('-')
|
35
|
+
end
|
36
|
+
end
|