geminabox 3.1.1 → 4.0.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.
- 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 +5 -5
- data/lib/geminabox/incoming_gem.rb +1 -5
- data/lib/geminabox/server.rb +66 -56
- 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 -102
- data/lib/geminabox/proxy/hostess.rb +0 -85
- 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,10 +9,7 @@ module Geminabox
|
|
|
9
9
|
|
|
10
10
|
def serve
|
|
11
11
|
headers["Cache-Control"] = 'no-transform'
|
|
12
|
-
|
|
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'])
|
|
12
|
+
send_file(File.expand_path(File.join(Geminabox.data, *request.path_info)), :type => response['Content-Type'])
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
%w[/specs.4.8.gz
|
|
@@ -26,7 +23,10 @@ module Geminabox
|
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
get '/quick/Marshal.4.8/*.gemspec.rz' do
|
|
29
|
-
|
|
26
|
+
# rubygems.org serves the deflated quick specs as octet-stream, and the
|
|
27
|
+
# compact-index conformance suite asserts it; clients inflate the body
|
|
28
|
+
# regardless of this header.
|
|
29
|
+
content_type('application/octet-stream')
|
|
30
30
|
serve
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -26,12 +26,8 @@ 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.
|
|
31
29
|
def valid?
|
|
32
|
-
spec && spec.name && spec.version
|
|
33
|
-
spec.name.match?(Gem::Specification::VALID_NAME_PATTERN) &&
|
|
34
|
-
File.basename(name) == name
|
|
30
|
+
spec && spec.name && spec.version
|
|
35
31
|
rescue Gem::Package::Error
|
|
36
32
|
false
|
|
37
33
|
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,9 +174,8 @@ module Geminabox
|
|
|
144
174
|
error_response(403, 'Gem deletion is disabled - see https://github.com/geminabox/geminabox/issues/115')
|
|
145
175
|
end
|
|
146
176
|
|
|
147
|
-
path = file_path
|
|
148
177
|
serialize_update do
|
|
149
|
-
File.delete
|
|
178
|
+
File.delete file_path if File.exist? file_path
|
|
150
179
|
self.class.reindex(:force_rebuild)
|
|
151
180
|
redirect url("/")
|
|
152
181
|
end
|
|
@@ -161,8 +190,16 @@ module Geminabox
|
|
|
161
190
|
halt 400 unless request.form_data?
|
|
162
191
|
|
|
163
192
|
serialize_update do
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
# A yank targets one platform. The client omits the platform param for
|
|
194
|
+
# a plain-ruby gem, so an absent/blank value means the ruby platform;
|
|
195
|
+
# without this a `gem yank foo -v 1.0.0` would also destroy foo's java
|
|
196
|
+
# and mingw builds sharing that version.
|
|
197
|
+
requested_platform = params['platform'].to_s.empty? ? 'ruby' : params['platform']
|
|
198
|
+
gems = load_gems.select do |gem|
|
|
199
|
+
params['gem_name'] == gem.name &&
|
|
200
|
+
params['version'] == gem.number.version &&
|
|
201
|
+
(gem.ruby? ? 'ruby' : gem.platform) == requested_platform
|
|
202
|
+
end
|
|
166
203
|
halt 404, 'Gem not found' if gems.size == 0
|
|
167
204
|
gems.each do |gem|
|
|
168
205
|
gem_path = File.expand_path(File.join(Geminabox.data, 'gems',
|
|
@@ -217,6 +254,22 @@ module Geminabox
|
|
|
217
254
|
self.class.with_rlock(&block)
|
|
218
255
|
end
|
|
219
256
|
|
|
257
|
+
def bootstrap_compact_index(path)
|
|
258
|
+
return if File.exist?(path)
|
|
259
|
+
serialize_update { self.class.compact_indexer.reindex }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Rebuild info/NAME on the read path when it is missing but versions.list
|
|
263
|
+
# still lists the gem, so a partially corrupted index does not 404 a gem
|
|
264
|
+
# Bundler was told exists. The ledger-membership check reads only
|
|
265
|
+
# versions.list and takes no lock, so an unknown name stays a cheap 404
|
|
266
|
+
# with no per-gem stats -- arbitrary /info probes cannot force writes.
|
|
267
|
+
def heal_missing_info(name, path)
|
|
268
|
+
indexer = self.class.compact_indexer
|
|
269
|
+
return unless indexer.ledger_lists?(name)
|
|
270
|
+
serialize_update { indexer.heal_info(name) unless File.file?(path) }
|
|
271
|
+
end
|
|
272
|
+
|
|
220
273
|
def handle_incoming_gem(gem)
|
|
221
274
|
begin
|
|
222
275
|
GemStore.create(gem, params[:overwrite])
|
|
@@ -256,46 +309,15 @@ HTML
|
|
|
256
309
|
end
|
|
257
310
|
|
|
258
311
|
def file_path
|
|
259
|
-
|
|
260
|
-
# Rack::Protection normally strips ".." first; this holds without it.
|
|
261
|
-
halt 404 unless path.start_with?(File.expand_path(Geminabox.data) + File::SEPARATOR)
|
|
262
|
-
path
|
|
312
|
+
File.expand_path(File.join(Geminabox.data, *request.path_info))
|
|
263
313
|
end
|
|
264
314
|
|
|
265
315
|
def dependency_cache
|
|
266
316
|
self.class.dependency_cache
|
|
267
317
|
end
|
|
268
318
|
|
|
269
|
-
def all_gems
|
|
270
|
-
all_gems_with_duplicates.inject(:|)
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def all_gems_with_duplicates
|
|
274
|
-
specs_files_paths.map do |specs_file_path|
|
|
275
|
-
if File.exist?(specs_file_path)
|
|
276
|
-
Marshal.load(Gem::Util.gunzip(Gem.read_binary(specs_file_path)))
|
|
277
|
-
else
|
|
278
|
-
[]
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def specs_file_types
|
|
284
|
-
[:specs, :prerelease_specs]
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def specs_files_paths
|
|
288
|
-
specs_file_types.map do |specs_file_type|
|
|
289
|
-
File.join(Geminabox.data, spec_file_name(specs_file_type))
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def spec_file_name(specs_file_type)
|
|
294
|
-
[specs_file_type, Gem.marshal_version, 'gz'].join('.')
|
|
295
|
-
end
|
|
296
|
-
|
|
297
319
|
def load_gems
|
|
298
|
-
@loaded_gems ||= Geminabox::GemVersionCollection.
|
|
320
|
+
@loaded_gems ||= Geminabox::GemVersionCollection.from_specs_index(Geminabox.data)
|
|
299
321
|
end
|
|
300
322
|
|
|
301
323
|
def index_gems(gems)
|
|
@@ -303,25 +325,13 @@ HTML
|
|
|
303
325
|
end
|
|
304
326
|
|
|
305
327
|
def gem_list
|
|
306
|
-
|
|
328
|
+
query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
|
|
307
329
|
end
|
|
308
330
|
|
|
309
331
|
def query_gems
|
|
310
332
|
params[:gems].to_s.split(',')
|
|
311
333
|
end
|
|
312
334
|
|
|
313
|
-
def local_gem_list
|
|
314
|
-
query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def remote_gem_list
|
|
318
|
-
RubygemsDependency.for(*query_gems)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def combined_gem_list
|
|
322
|
-
GemListMerge.merge(local_gem_list, remote_gem_list, strategy: Geminabox.rubygems_proxy_merge_strategy)
|
|
323
|
-
end
|
|
324
|
-
|
|
325
335
|
helpers do
|
|
326
336
|
def href(text)
|
|
327
337
|
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
|