geminabox 3.1.0 → 4.0.1
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.
- checksums.yaml +4 -4
- data/README.md +149 -58
- data/lib/geminabox/compact_index_api.rb +77 -0
- data/lib/geminabox/compact_indexer.rb +280 -0
- data/lib/geminabox/gem_version.rb +8 -0
- data/lib/geminabox/gem_version_collection.rb +12 -0
- data/lib/geminabox/hostess.rb +8 -2
- data/lib/geminabox/incoming_gem.rb +5 -1
- data/lib/geminabox/server.rb +70 -52
- data/lib/geminabox/version.rb +1 -1
- data/lib/geminabox.rb +28 -24
- data/public/master.css +377 -107
- data/public/master.js +133 -10
- data/views/_version_row.erb +16 -0
- data/views/gem.erb +14 -27
- data/views/index.erb +43 -48
- data/views/layout.erb +16 -0
- data/views/upload.erb +3 -5
- metadata +34 -11
- data/lib/geminabox/gem_list_merge.rb +0 -40
- data/lib/geminabox/proxy/copier.rb +0 -37
- data/lib/geminabox/proxy/file_handler.rb +0 -97
- data/lib/geminabox/proxy/hostess.rb +0 -78
- data/lib/geminabox/proxy/splicer.rb +0 -85
- data/lib/geminabox/proxy.rb +0 -14
- data/lib/geminabox/rubygems_dependency.rb +0 -32
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'rubygems/util'
|
|
4
|
+
|
|
3
5
|
module Geminabox
|
|
4
6
|
|
|
5
7
|
# This class represents a sorted collection of Geminabox::GemVersion objects.
|
|
@@ -7,6 +9,16 @@ module Geminabox
|
|
|
7
9
|
class GemVersionCollection
|
|
8
10
|
include Enumerable
|
|
9
11
|
|
|
12
|
+
# Loads the collection recorded in the legacy Marshal specs indexes
|
|
13
|
+
# (specs.4.8.gz and prerelease_specs.4.8.gz) under data_dir.
|
|
14
|
+
def self.from_specs_index(data_dir)
|
|
15
|
+
specs = %w[specs prerelease_specs].flat_map do |type|
|
|
16
|
+
path = File.join(data_dir, "#{type}.#{Gem.marshal_version}.gz")
|
|
17
|
+
File.exist?(path) ? Marshal.load(Gem::Util.gunzip(Gem.read_binary(path))) : []
|
|
18
|
+
end
|
|
19
|
+
new(specs.uniq)
|
|
20
|
+
end
|
|
21
|
+
|
|
10
22
|
# Array of Geminabox::GemVersion objects, or an array of [name, version,
|
|
11
23
|
# platform] triples.
|
|
12
24
|
def initialize(initial_gems=[])
|
data/lib/geminabox/hostess.rb
CHANGED
|
@@ -9,7 +9,10 @@ module Geminabox
|
|
|
9
9
|
|
|
10
10
|
def serve
|
|
11
11
|
headers["Cache-Control"] = 'no-transform'
|
|
12
|
-
|
|
12
|
+
path = File.expand_path(File.join(Geminabox.data, *request.path_info))
|
|
13
|
+
# Rack::Protection normally strips ".." first; this holds without it.
|
|
14
|
+
halt 404 unless path.start_with?(File.expand_path(Geminabox.data) + File::SEPARATOR)
|
|
15
|
+
send_file(path, :type => response['Content-Type'])
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
%w[/specs.4.8.gz
|
|
@@ -23,7 +26,10 @@ module Geminabox
|
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
get '/quick/Marshal.4.8/*.gemspec.rz' do
|
|
26
|
-
|
|
29
|
+
# rubygems.org serves the deflated quick specs as octet-stream, and the
|
|
30
|
+
# compact-index conformance suite asserts it; clients inflate the body
|
|
31
|
+
# regardless of this header.
|
|
32
|
+
content_type('application/octet-stream')
|
|
27
33
|
serve
|
|
28
34
|
end
|
|
29
35
|
|
|
@@ -26,8 +26,12 @@ module Geminabox
|
|
|
26
26
|
File.open(@tempfile.path, "rb")
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# spec comes from the uploaded archive and RubyGems does not validate it on
|
|
30
|
+
# read, so the name must be checked before it becomes part of a path.
|
|
29
31
|
def valid?
|
|
30
|
-
spec && spec.name && spec.version
|
|
32
|
+
spec && spec.name && spec.version &&
|
|
33
|
+
spec.name.match?(Gem::Specification::VALID_NAME_PATTERN) &&
|
|
34
|
+
File.basename(name) == name
|
|
31
35
|
rescue Gem::Package::Error
|
|
32
36
|
false
|
|
33
37
|
end
|
data/lib/geminabox/server.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'reentrant_flock'
|
|
4
4
|
require 'rubygems/util'
|
|
5
|
+
require 'rss'
|
|
5
6
|
|
|
6
7
|
module Geminabox
|
|
7
8
|
|
|
@@ -11,12 +12,7 @@ module Geminabox
|
|
|
11
12
|
set :views, Geminabox.views
|
|
12
13
|
set :host_authorization, { permitted_hosts: [] }
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
Geminabox.warn_rubygems_proxy_deprecation
|
|
16
|
-
use Proxy::Hostess
|
|
17
|
-
else
|
|
18
|
-
use Hostess
|
|
19
|
-
end
|
|
15
|
+
use Hostess
|
|
20
16
|
|
|
21
17
|
class << self
|
|
22
18
|
def disallow_replace?
|
|
@@ -43,6 +39,7 @@ module Geminabox
|
|
|
43
39
|
if force_rebuild
|
|
44
40
|
indexer.generate_index
|
|
45
41
|
dependency_cache.flush
|
|
42
|
+
compact_indexer.reindex
|
|
46
43
|
else
|
|
47
44
|
begin
|
|
48
45
|
require 'geminabox/indexer'
|
|
@@ -50,6 +47,7 @@ module Geminabox
|
|
|
50
47
|
return if updated_gemspecs.empty?
|
|
51
48
|
indexer.update_index
|
|
52
49
|
updated_gemspecs.each { |gem| dependency_cache.flush_key(gem.name) }
|
|
50
|
+
compact_indexer.reindex
|
|
53
51
|
rescue Errno::ENOENT
|
|
54
52
|
with_rlock { reindex(:force_rebuild) }
|
|
55
53
|
rescue => e
|
|
@@ -69,6 +67,10 @@ module Geminabox
|
|
|
69
67
|
@dependency_cache ||= Geminabox::DiskCache.new(File.join(Geminabox.data, "_cache"))
|
|
70
68
|
end
|
|
71
69
|
|
|
70
|
+
def compact_indexer
|
|
71
|
+
Geminabox::CompactIndexer.new(Geminabox.data)
|
|
72
|
+
end
|
|
73
|
+
|
|
72
74
|
def with_rlock(&block)
|
|
73
75
|
file_class.open(Geminabox.lockfile, File::RDWR | File::CREAT) do |f|
|
|
74
76
|
ReentrantFlock.synchronize(f, File::LOCK_EX | File::LOCK_NB, &block)
|
|
@@ -91,6 +93,7 @@ module Geminabox
|
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
get '/' do
|
|
96
|
+
content_type :html
|
|
94
97
|
@gems = load_gems
|
|
95
98
|
@index_gems = index_gems(@gems)
|
|
96
99
|
@allow_upload = self.class.allow_upload?
|
|
@@ -99,15 +102,40 @@ module Geminabox
|
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
get '/atom.xml' do
|
|
105
|
+
content_type 'application/atom+xml'
|
|
102
106
|
@gems = load_gems
|
|
103
107
|
erb :atom, :layout => false
|
|
104
108
|
end
|
|
105
109
|
|
|
110
|
+
helpers Geminabox::CompactIndexApi
|
|
111
|
+
|
|
112
|
+
get '/versions' do
|
|
113
|
+
bootstrap_compact_index(self.class.compact_indexer.versions_path)
|
|
114
|
+
serve_compact_file(self.class.compact_indexer.versions_path)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
get '/names' do
|
|
118
|
+
bootstrap_compact_index(self.class.compact_indexer.names_path)
|
|
119
|
+
serve_compact_file(self.class.compact_indexer.names_path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
get '/info/:name' do
|
|
123
|
+
# Reject anything that is not a gem-name shape before touching the
|
|
124
|
+
# filesystem. This also blocks NUL bytes and empty names, which would
|
|
125
|
+
# otherwise raise ArgumentError from File.file? and 500.
|
|
126
|
+
halt 404 unless params[:name] =~ /\A[a-zA-Z0-9_.-]+\z/
|
|
127
|
+
path = self.class.compact_indexer.info_path(params[:name])
|
|
128
|
+
heal_missing_info(params[:name], path) unless File.file?(path)
|
|
129
|
+
serve_compact_file(path)
|
|
130
|
+
end
|
|
131
|
+
|
|
106
132
|
get '/api/v1/dependencies' do
|
|
133
|
+
content_type 'application/octet-stream'
|
|
107
134
|
query_gems.any? ? Marshal.dump(gem_list) : 200
|
|
108
135
|
end
|
|
109
136
|
|
|
110
137
|
get '/api/v1/dependencies.json' do
|
|
138
|
+
content_type :json
|
|
111
139
|
query_gems.any? ? gem_list.to_json : {}
|
|
112
140
|
end
|
|
113
141
|
|
|
@@ -116,6 +144,7 @@ module Geminabox
|
|
|
116
144
|
error_response(403, 'Gem uploading is disabled')
|
|
117
145
|
end
|
|
118
146
|
|
|
147
|
+
content_type :html
|
|
119
148
|
erb :upload
|
|
120
149
|
end
|
|
121
150
|
|
|
@@ -136,6 +165,7 @@ module Geminabox
|
|
|
136
165
|
@gem = gems[params[:gemname]]
|
|
137
166
|
@allow_delete = self.class.allow_delete?
|
|
138
167
|
halt 404 unless @gem
|
|
168
|
+
content_type :html
|
|
139
169
|
erb :gem
|
|
140
170
|
end
|
|
141
171
|
|
|
@@ -144,8 +174,9 @@ module Geminabox
|
|
|
144
174
|
error_response(403, 'Gem deletion is disabled - see https://github.com/geminabox/geminabox/issues/115')
|
|
145
175
|
end
|
|
146
176
|
|
|
177
|
+
path = file_path
|
|
147
178
|
serialize_update do
|
|
148
|
-
File.delete
|
|
179
|
+
File.delete path if File.exist? path
|
|
149
180
|
self.class.reindex(:force_rebuild)
|
|
150
181
|
redirect url("/")
|
|
151
182
|
end
|
|
@@ -160,8 +191,16 @@ module Geminabox
|
|
|
160
191
|
halt 400 unless request.form_data?
|
|
161
192
|
|
|
162
193
|
serialize_update do
|
|
163
|
-
|
|
164
|
-
|
|
194
|
+
# A yank targets one platform. The client omits the platform param for
|
|
195
|
+
# a plain-ruby gem, so an absent/blank value means the ruby platform;
|
|
196
|
+
# without this a `gem yank foo -v 1.0.0` would also destroy foo's java
|
|
197
|
+
# and mingw builds sharing that version.
|
|
198
|
+
requested_platform = params['platform'].to_s.empty? ? 'ruby' : params['platform']
|
|
199
|
+
gems = load_gems.select do |gem|
|
|
200
|
+
params['gem_name'] == gem.name &&
|
|
201
|
+
params['version'] == gem.number.version &&
|
|
202
|
+
(gem.ruby? ? 'ruby' : gem.platform) == requested_platform
|
|
203
|
+
end
|
|
165
204
|
halt 404, 'Gem not found' if gems.size == 0
|
|
166
205
|
gems.each do |gem|
|
|
167
206
|
gem_path = File.expand_path(File.join(Geminabox.data, 'gems',
|
|
@@ -216,6 +255,22 @@ module Geminabox
|
|
|
216
255
|
self.class.with_rlock(&block)
|
|
217
256
|
end
|
|
218
257
|
|
|
258
|
+
def bootstrap_compact_index(path)
|
|
259
|
+
return if File.exist?(path)
|
|
260
|
+
serialize_update { self.class.compact_indexer.reindex }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Rebuild info/NAME on the read path when it is missing but versions.list
|
|
264
|
+
# still lists the gem, so a partially corrupted index does not 404 a gem
|
|
265
|
+
# Bundler was told exists. The ledger-membership check reads only
|
|
266
|
+
# versions.list and takes no lock, so an unknown name stays a cheap 404
|
|
267
|
+
# with no per-gem stats -- arbitrary /info probes cannot force writes.
|
|
268
|
+
def heal_missing_info(name, path)
|
|
269
|
+
indexer = self.class.compact_indexer
|
|
270
|
+
return unless indexer.ledger_lists?(name)
|
|
271
|
+
serialize_update { indexer.heal_info(name) unless File.file?(path) }
|
|
272
|
+
end
|
|
273
|
+
|
|
219
274
|
def handle_incoming_gem(gem)
|
|
220
275
|
begin
|
|
221
276
|
GemStore.create(gem, params[:overwrite])
|
|
@@ -255,43 +310,18 @@ HTML
|
|
|
255
310
|
end
|
|
256
311
|
|
|
257
312
|
def file_path
|
|
258
|
-
File.expand_path(File.join(Geminabox.data, *request.path_info))
|
|
313
|
+
path = File.expand_path(File.join(Geminabox.data, *request.path_info))
|
|
314
|
+
# Rack::Protection normally strips ".." first; this holds without it.
|
|
315
|
+
halt 404 unless path.start_with?(File.expand_path(Geminabox.data) + File::SEPARATOR)
|
|
316
|
+
path
|
|
259
317
|
end
|
|
260
318
|
|
|
261
319
|
def dependency_cache
|
|
262
320
|
self.class.dependency_cache
|
|
263
321
|
end
|
|
264
322
|
|
|
265
|
-
def all_gems
|
|
266
|
-
all_gems_with_duplicates.inject(:|)
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def all_gems_with_duplicates
|
|
270
|
-
specs_files_paths.map do |specs_file_path|
|
|
271
|
-
if File.exist?(specs_file_path)
|
|
272
|
-
Marshal.load(Gem::Util.gunzip(Gem.read_binary(specs_file_path)))
|
|
273
|
-
else
|
|
274
|
-
[]
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def specs_file_types
|
|
280
|
-
[:specs, :prerelease_specs]
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def specs_files_paths
|
|
284
|
-
specs_file_types.map do |specs_file_type|
|
|
285
|
-
File.join(Geminabox.data, spec_file_name(specs_file_type))
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def spec_file_name(specs_file_type)
|
|
290
|
-
[specs_file_type, Gem.marshal_version, 'gz'].join('.')
|
|
291
|
-
end
|
|
292
|
-
|
|
293
323
|
def load_gems
|
|
294
|
-
@loaded_gems ||= Geminabox::GemVersionCollection.
|
|
324
|
+
@loaded_gems ||= Geminabox::GemVersionCollection.from_specs_index(Geminabox.data)
|
|
295
325
|
end
|
|
296
326
|
|
|
297
327
|
def index_gems(gems)
|
|
@@ -299,25 +329,13 @@ HTML
|
|
|
299
329
|
end
|
|
300
330
|
|
|
301
331
|
def gem_list
|
|
302
|
-
|
|
332
|
+
query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
|
|
303
333
|
end
|
|
304
334
|
|
|
305
335
|
def query_gems
|
|
306
336
|
params[:gems].to_s.split(',')
|
|
307
337
|
end
|
|
308
338
|
|
|
309
|
-
def local_gem_list
|
|
310
|
-
query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def remote_gem_list
|
|
314
|
-
RubygemsDependency.for(*query_gems)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def combined_gem_list
|
|
318
|
-
GemListMerge.merge(local_gem_list, remote_gem_list, strategy: Geminabox.rubygems_proxy_merge_strategy)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
339
|
helpers do
|
|
322
340
|
def href(text)
|
|
323
341
|
if text && (text.start_with?('http://') || text.start_with?('https://'))
|
data/lib/geminabox/version.rb
CHANGED
data/lib/geminabox.rb
CHANGED
|
@@ -17,7 +17,6 @@ module Geminabox
|
|
|
17
17
|
class Error < StandardError ; end
|
|
18
18
|
|
|
19
19
|
require_relative 'geminabox/version'
|
|
20
|
-
require_relative 'geminabox/proxy'
|
|
21
20
|
require_relative 'geminabox/http_adapter'
|
|
22
21
|
|
|
23
22
|
def self.geminabox_path(file)
|
|
@@ -27,10 +26,10 @@ module Geminabox
|
|
|
27
26
|
autoload :Hostess, geminabox_path('hostess')
|
|
28
27
|
autoload :GemStore, geminabox_path('gem_store')
|
|
29
28
|
autoload :GemStoreError, geminabox_path('gem_store_error')
|
|
30
|
-
autoload :RubygemsDependency, geminabox_path('rubygems_dependency')
|
|
31
|
-
autoload :GemListMerge, geminabox_path('gem_list_merge')
|
|
32
29
|
autoload :GemVersion, geminabox_path('gem_version')
|
|
33
30
|
autoload :GemVersionCollection, geminabox_path('gem_version_collection')
|
|
31
|
+
autoload :CompactIndexer, geminabox_path('compact_indexer')
|
|
32
|
+
autoload :CompactIndexApi, geminabox_path('compact_index_api')
|
|
34
33
|
autoload :Server, geminabox_path('server')
|
|
35
34
|
autoload :DiskCache, geminabox_path('disk_cache')
|
|
36
35
|
autoload :IncomingGem, geminabox_path('incoming_gem')
|
|
@@ -45,14 +44,9 @@ module Geminabox
|
|
|
45
44
|
:allow_replace,
|
|
46
45
|
:gem_permissions,
|
|
47
46
|
:allow_delete,
|
|
48
|
-
:rubygems_proxy,
|
|
49
|
-
:rubygems_proxy_merge_strategy,
|
|
50
47
|
:http_adapter,
|
|
51
48
|
:lockfile,
|
|
52
49
|
:retry_interval,
|
|
53
|
-
:allow_remote_failure,
|
|
54
|
-
:ruby_gems_url,
|
|
55
|
-
:bundler_ruby_gems_url,
|
|
56
50
|
:allow_upload,
|
|
57
51
|
:on_gem_received
|
|
58
52
|
)
|
|
@@ -68,17 +62,6 @@ module Geminabox
|
|
|
68
62
|
Server.settings
|
|
69
63
|
end
|
|
70
64
|
|
|
71
|
-
# Deprecated: the RubyGems proxy is unmaintained and will be removed in
|
|
72
|
-
# Geminabox 4.0. It depends on the RubyGems.org Dependency API
|
|
73
|
-
# (/api/v1/dependencies), which was sunset on 2023-05-24, so proxy mode no
|
|
74
|
-
# longer functions.
|
|
75
|
-
def warn_rubygems_proxy_deprecation
|
|
76
|
-
warn '[DEPRECATION] Geminabox RubyGems proxy is deprecated and will be ' \
|
|
77
|
-
'removed in Geminabox 4.0. It depends on the RubyGems.org ' \
|
|
78
|
-
'Dependency API (/api/v1/dependencies), which was sunset on ' \
|
|
79
|
-
'2023-05-24, so proxy mode no longer functions.'
|
|
80
|
-
end
|
|
81
|
-
|
|
82
65
|
def call(env)
|
|
83
66
|
Server.call env
|
|
84
67
|
end
|
|
@@ -91,17 +74,38 @@ module Geminabox
|
|
|
91
74
|
views: File.join(File.dirname(__FILE__), *%w[.. views]),
|
|
92
75
|
allow_replace: false,
|
|
93
76
|
gem_permissions: 0644,
|
|
94
|
-
rubygems_proxy: (ENV['RUBYGEMS_PROXY'] == 'true'),
|
|
95
|
-
rubygems_proxy_merge_strategy: ENV.fetch('RUBYGEMS_PROXY_MERGE_STRATEGY') { :local_gems_take_precedence_over_remote_gems }.to_sym,
|
|
96
77
|
allow_delete: true,
|
|
97
78
|
http_adapter: HttpClientAdapter.new,
|
|
98
79
|
lockfile: File.join(ENV.fetch('TMPDIR', Dir.tmpdir), 'geminabox.lockfile'),
|
|
99
80
|
retry_interval: 60,
|
|
100
|
-
allow_remote_failure: false,
|
|
101
|
-
ruby_gems_url: 'https://rubygems.org/',
|
|
102
|
-
bundler_ruby_gems_url: 'https://bundler.rubygems.org/',
|
|
103
81
|
allow_upload: true,
|
|
104
82
|
on_gem_received: nil
|
|
105
83
|
)
|
|
106
84
|
|
|
85
|
+
# Removed in 4.0 with the RubyGems proxy. Warn-and-ignore (rather than
|
|
86
|
+
# NoMethodError at boot) so stale config.ru lines don't break upgrades.
|
|
87
|
+
# Drop these shims in 5.0.
|
|
88
|
+
[
|
|
89
|
+
:rubygems_proxy,
|
|
90
|
+
:rubygems_proxy_merge_strategy,
|
|
91
|
+
:allow_remote_failure,
|
|
92
|
+
:ruby_gems_url,
|
|
93
|
+
:bundler_ruby_gems_url
|
|
94
|
+
].each do |setting|
|
|
95
|
+
define_singleton_method("#{setting}=") do |_value|
|
|
96
|
+
warn "[REMOVED] Geminabox.#{setting} was removed in Geminabox 4.0 " \
|
|
97
|
+
'along with the RubyGems proxy and has no effect. Remove this ' \
|
|
98
|
+
'line from your config.ru. Migration notes: ' \
|
|
99
|
+
'https://github.com/geminabox/geminabox/pull/735'
|
|
100
|
+
end
|
|
101
|
+
define_singleton_method(setting) { nil }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if ENV['RUBYGEMS_PROXY'] == 'true' || ENV.key?('RUBYGEMS_PROXY_MERGE_STRATEGY')
|
|
105
|
+
warn '[REMOVED] The RUBYGEMS_PROXY and RUBYGEMS_PROXY_MERGE_STRATEGY ' \
|
|
106
|
+
'environment variables were removed in Geminabox 4.0; Geminabox ' \
|
|
107
|
+
'now always serves only local gems. Migration notes: ' \
|
|
108
|
+
'https://github.com/geminabox/geminabox/pull/735'
|
|
109
|
+
end
|
|
110
|
+
|
|
107
111
|
end
|