Class: Paquette::GemServer::DirectoryGemRepository

Inherits:
GemRepository
  • Object
show all
Defined in:
lib/paquette/gem_server/directory_gem_repository.rb

Overview

Repository implementation that reads gems from a directory

Defined Under Namespace

Classes: GemAlreadyExists, GemNotFound, GemYanked, InvalidGem

Instance Method Summary collapse

Constructor Details

#initialize(gems_dir) ⇒ DirectoryGemRepository

Returns a new instance of DirectoryGemRepository.



17
18
19
20
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 17

def initialize(gems_dir)
  @gems_dir = gems_dir
  FileUtils.mkdir_p(@gems_dir)
end

Instance Method Details

#add_gem(binary_data) ⇒ Object

Persists a .gem file from its raw binary contents. Returns the parsed spec on success. Raises InvalidGem when the payload can’t be opened as a gem, or GemAlreadyExists if the name+version is already on disk.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 25

def add_gem(binary_data)
  raise InvalidGem, "Empty gem payload" if binary_data.nil? || binary_data.empty?

  tmp = Tempfile.new(["paquette_push", ".gem"])
  tmp.binmode
  tmp.write(binary_data)
  tmp.close

  spec = begin
    Gem::Package.new(tmp.path).spec
  rescue Gem::Package::Error, StandardError => e
    raise InvalidGem, "Could not read gem: #{e.message}"
  end

  name = spec.name
  version = spec.version.to_s
  raise GemYanked, "#{name}-#{version} was yanked and cannot be republished" if tomb_exists?(name, version)
  raise GemAlreadyExists, "#{name}-#{version} already exists" if gem_exists?(name, version)

  dest_dir = File.join(@gems_dir, name)
  FileUtils.mkdir_p(dest_dir)
  FileUtils.mv(tmp.path, gem_file_path(name, version))

  spec
ensure
  tmp&.close unless tmp&.closed?
  File.unlink(tmp.path) if tmp && File.exist?(tmp.path)
end

#compact_info(gem_name) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 131

def compact_info(gem_name)
  versions = versions_for_gem(gem_name)
  return [] if versions.empty?

  require "digest"

  versions.map do |version|
    spec = gem_spec(gem_name, version)
    next unless spec

    # Calculate SHA256 checksum of the gem file
    gem_file = gem_file_path(gem_name, version)
    checksum = Digest::SHA256.file(gem_file).hexdigest

    # Get required Ruby version from gemspec
    ruby_version = spec.required_ruby_version&.to_s || ">= 0"

    # Format: version |checksum:sha256_checksum,ruby:required_ruby_version
    "#{version} |checksum:#{checksum},ruby:#{ruby_version}"
  end.compact
end

#gem_dependencies(gem_name, version) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 117

def gem_dependencies(gem_name, version)
  spec = gem_spec(gem_name, version)
  return [] unless spec

  # Only include runtime dependencies, not development dependencies
  runtime_deps = spec.dependencies.select { |dep| dep.type == :runtime }
  runtime_deps.map do |dep|
    {
      name: dep.name,
      requirements: dep.requirement.to_s
    }
  end
end

#gem_exists?(gem_name, version) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 105

def gem_exists?(gem_name, version)
  File.exist?(gem_file_path(gem_name, version))
end

#gem_file_path(gem_name, version) ⇒ Object



101
102
103
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 101

def gem_file_path(gem_name, version)
  File.join(@gems_dir, gem_name, "#{gem_name}-#{version}.gem")
end

#gem_namesObject



73
74
75
76
77
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 73

def gem_names
  Dir.glob(File.join(@gems_dir, "*")).select { |path| File.directory?(path) }.map do |package_path|
    File.basename(package_path)
  end.sort
end

#gem_spec(gem_name, version) ⇒ Object



109
110
111
112
113
114
115
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 109

def gem_spec(gem_name, version)
  gem_file = gem_file_path(gem_name, version)
  return nil unless File.exist?(gem_file)

  pkg = Gem::Package.new(gem_file)
  pkg.spec
end

#gem_versionsObject



79
80
81
82
83
84
85
86
87
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 79

def gem_versions
  versions = []
  gem_names.each do |gem_name|
    versions_for_gem(gem_name).each do |version|
      versions << [gem_name, version]
    end
  end
  versions.sort
end

#tomb_exists?(gem_name, version) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 69

def tomb_exists?(gem_name, version)
  File.exist?(tomb_file_path(gem_name, version))
end

#tomb_file_path(gem_name, version) ⇒ Object



65
66
67
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 65

def tomb_file_path(gem_name, version)
  gem_file_path(gem_name, version) + ".tomb"
end

#versions_for_gem(gem_name) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 89

def versions_for_gem(gem_name)
  gem_dir = File.join(@gems_dir, gem_name)
  return [] unless Dir.exist?(gem_dir)

  Dir.glob(File.join(gem_dir, "*.gem")).map do |gem_path|
    filename = File.basename(gem_path, ".gem")
    if (match = filename.match(/^#{Regexp.escape(gem_name)}-(\d+\.\d+\.\d+.*)$/))
      match[1]
    end
  end.compact.sort
end

#yank_gem(gem_name, version) ⇒ Object

Yanks a gem by renaming its .gem file to .gem.tomb. The tomb prevents the same name+version from being re-pushed later. Raises GemNotFound when the gem was never present or already yanked.

Raises:



57
58
59
60
61
62
63
# File 'lib/paquette/gem_server/directory_gem_repository.rb', line 57

def yank_gem(gem_name, version)
  gem_path = gem_file_path(gem_name, version)
  raise GemNotFound, "#{gem_name}-#{version} not found" unless File.exist?(gem_path)

  FileUtils.mv(gem_path, tomb_file_path(gem_name, version))
  nil
end