geminabox 3.0.0 → 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.
@@ -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=[])
@@ -23,7 +23,10 @@ module Geminabox
23
23
  end
24
24
 
25
25
  get '/quick/Marshal.4.8/*.gemspec.rz' do
26
- content_type('application/x-deflate')
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')
27
30
  serve
28
31
  end
29
32
 
@@ -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,11 +12,7 @@ module Geminabox
11
12
  set :views, Geminabox.views
12
13
  set :host_authorization, { permitted_hosts: [] }
13
14
 
14
- if Geminabox.rubygems_proxy
15
- use Proxy::Hostess
16
- else
17
- use Hostess
18
- end
15
+ use Hostess
19
16
 
20
17
  class << self
21
18
  def disallow_replace?
@@ -42,6 +39,7 @@ module Geminabox
42
39
  if force_rebuild
43
40
  indexer.generate_index
44
41
  dependency_cache.flush
42
+ compact_indexer.reindex
45
43
  else
46
44
  begin
47
45
  require 'geminabox/indexer'
@@ -49,6 +47,7 @@ module Geminabox
49
47
  return if updated_gemspecs.empty?
50
48
  indexer.update_index
51
49
  updated_gemspecs.each { |gem| dependency_cache.flush_key(gem.name) }
50
+ compact_indexer.reindex
52
51
  rescue Errno::ENOENT
53
52
  with_rlock { reindex(:force_rebuild) }
54
53
  rescue => e
@@ -68,6 +67,10 @@ module Geminabox
68
67
  @dependency_cache ||= Geminabox::DiskCache.new(File.join(Geminabox.data, "_cache"))
69
68
  end
70
69
 
70
+ def compact_indexer
71
+ Geminabox::CompactIndexer.new(Geminabox.data)
72
+ end
73
+
71
74
  def with_rlock(&block)
72
75
  file_class.open(Geminabox.lockfile, File::RDWR | File::CREAT) do |f|
73
76
  ReentrantFlock.synchronize(f, File::LOCK_EX | File::LOCK_NB, &block)
@@ -90,6 +93,7 @@ module Geminabox
90
93
  end
91
94
 
92
95
  get '/' do
96
+ content_type :html
93
97
  @gems = load_gems
94
98
  @index_gems = index_gems(@gems)
95
99
  @allow_upload = self.class.allow_upload?
@@ -98,15 +102,40 @@ module Geminabox
98
102
  end
99
103
 
100
104
  get '/atom.xml' do
105
+ content_type 'application/atom+xml'
101
106
  @gems = load_gems
102
107
  erb :atom, :layout => false
103
108
  end
104
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
+
105
132
  get '/api/v1/dependencies' do
133
+ content_type 'application/octet-stream'
106
134
  query_gems.any? ? Marshal.dump(gem_list) : 200
107
135
  end
108
136
 
109
137
  get '/api/v1/dependencies.json' do
138
+ content_type :json
110
139
  query_gems.any? ? gem_list.to_json : {}
111
140
  end
112
141
 
@@ -115,6 +144,7 @@ module Geminabox
115
144
  error_response(403, 'Gem uploading is disabled')
116
145
  end
117
146
 
147
+ content_type :html
118
148
  erb :upload
119
149
  end
120
150
 
@@ -135,6 +165,7 @@ module Geminabox
135
165
  @gem = gems[params[:gemname]]
136
166
  @allow_delete = self.class.allow_delete?
137
167
  halt 404 unless @gem
168
+ content_type :html
138
169
  erb :gem
139
170
  end
140
171
 
@@ -159,8 +190,16 @@ module Geminabox
159
190
  halt 400 unless request.form_data?
160
191
 
161
192
  serialize_update do
162
- gems = load_gems.select { |gem| params['gem_name'] == gem.name and
163
- params['version'] == gem.number.version }
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
164
203
  halt 404, 'Gem not found' if gems.size == 0
165
204
  gems.each do |gem|
166
205
  gem_path = File.expand_path(File.join(Geminabox.data, 'gems',
@@ -215,6 +254,22 @@ module Geminabox
215
254
  self.class.with_rlock(&block)
216
255
  end
217
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
+
218
273
  def handle_incoming_gem(gem)
219
274
  begin
220
275
  GemStore.create(gem, params[:overwrite])
@@ -261,36 +316,8 @@ HTML
261
316
  self.class.dependency_cache
262
317
  end
263
318
 
264
- def all_gems
265
- all_gems_with_duplicates.inject(:|)
266
- end
267
-
268
- def all_gems_with_duplicates
269
- specs_files_paths.map do |specs_file_path|
270
- if File.exist?(specs_file_path)
271
- Marshal.load(Gem::Util.gunzip(Gem.read_binary(specs_file_path)))
272
- else
273
- []
274
- end
275
- end
276
- end
277
-
278
- def specs_file_types
279
- [:specs, :prerelease_specs]
280
- end
281
-
282
- def specs_files_paths
283
- specs_file_types.map do |specs_file_type|
284
- File.join(Geminabox.data, spec_file_name(specs_file_type))
285
- end
286
- end
287
-
288
- def spec_file_name(specs_file_type)
289
- [specs_file_type, Gem.marshal_version, 'gz'].join('.')
290
- end
291
-
292
319
  def load_gems
293
- @loaded_gems ||= Geminabox::GemVersionCollection.new(all_gems)
320
+ @loaded_gems ||= Geminabox::GemVersionCollection.from_specs_index(Geminabox.data)
294
321
  end
295
322
 
296
323
  def index_gems(gems)
@@ -298,25 +325,13 @@ HTML
298
325
  end
299
326
 
300
327
  def gem_list
301
- Geminabox.rubygems_proxy ? combined_gem_list : local_gem_list
328
+ query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
302
329
  end
303
330
 
304
331
  def query_gems
305
332
  params[:gems].to_s.split(',')
306
333
  end
307
334
 
308
- def local_gem_list
309
- query_gems.map{|query_gem| gem_dependencies(query_gem) }.flatten(1)
310
- end
311
-
312
- def remote_gem_list
313
- RubygemsDependency.for(*query_gems)
314
- end
315
-
316
- def combined_gem_list
317
- GemListMerge.merge(local_gem_list, remote_gem_list, strategy: Geminabox.rubygems_proxy_merge_strategy)
318
- end
319
-
320
335
  helpers do
321
336
  def href(text)
322
337
  if text && (text.start_with?('http://') || text.start_with?('https://'))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geminabox
4
- VERSION = '3.0.0' unless defined? VERSION
4
+ VERSION = '4.0.0' unless defined? VERSION
5
5
  end
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
  )
@@ -80,17 +74,38 @@ module Geminabox
80
74
  views: File.join(File.dirname(__FILE__), *%w[.. views]),
81
75
  allow_replace: false,
82
76
  gem_permissions: 0644,
83
- rubygems_proxy: (ENV['RUBYGEMS_PROXY'] == 'true'),
84
- rubygems_proxy_merge_strategy: ENV.fetch('RUBYGEMS_PROXY_MERGE_STRATEGY') { :local_gems_take_precedence_over_remote_gems }.to_sym,
85
77
  allow_delete: true,
86
78
  http_adapter: HttpClientAdapter.new,
87
79
  lockfile: File.join(ENV.fetch('TMPDIR', Dir.tmpdir), 'geminabox.lockfile'),
88
80
  retry_interval: 60,
89
- allow_remote_failure: false,
90
- ruby_gems_url: 'https://rubygems.org/',
91
- bundler_ruby_gems_url: 'https://bundler.rubygems.org/',
92
81
  allow_upload: true,
93
82
  on_gem_received: nil
94
83
  )
95
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
+
96
111
  end