gemvault 0.1.5 → 0.2.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/.containerignore +10 -0
- data/.rubocop.yml +8 -1
- data/CHANGELOG.md +30 -0
- data/CLAUDE.md +5 -4
- data/Dockerfile.test +11 -1
- data/README.md +3 -11
- data/Rakefile +9 -0
- data/issues.rec +540 -17
- data/lib/bundler/plugin/vault_source.rb +48 -73
- data/lib/bundler/plugin/vaulted_gem.rb +66 -0
- data/lib/gemvault/archive_entry.rb +6 -0
- data/lib/gemvault/cli/command.rb +9 -5
- data/lib/gemvault/cli/commands/extract.rb +11 -11
- data/lib/gemvault/cli/commands/new.rb +3 -2
- data/lib/gemvault/cli/commands/remove.rb +15 -5
- data/lib/gemvault/cli/commands/upgrade.rb +37 -13
- data/lib/gemvault/dbvault.rb +12 -9
- data/lib/gemvault/deprecation.rb +26 -24
- data/lib/gemvault/gem_entry.rb +20 -24
- data/lib/gemvault/gem_extraction.rb +19 -23
- data/lib/gemvault/gem_reference/any_version.rb +6 -2
- data/lib/gemvault/gem_reference/parser.rb +44 -0
- data/lib/gemvault/gem_reference/specific_version.rb +9 -25
- data/lib/gemvault/gem_reference.rb +13 -45
- data/lib/gemvault/manifest.rb +44 -42
- data/lib/gemvault/tarball.rb +53 -0
- data/lib/gemvault/tarvault.rb +47 -56
- data/lib/gemvault/vault.rb +17 -13
- data/lib/gemvault/vault_path.rb +33 -0
- data/lib/gemvault/vault_upgrade.rb +31 -24
- data/lib/gemvault/version.rb +1 -1
- data/lib/gemvault.rb +1 -0
- data/lib/rubygems/resolver/vault_set.rb +5 -7
- data/lib/rubygems/source/vault.rb +28 -32
- data/lib/rubygems_plugin.rb +8 -6
- metadata +7 -16
- data/lib/gemvault/tar_archive.rb +0 -66
|
@@ -4,42 +4,38 @@ require "stringio"
|
|
|
4
4
|
require_relative "vault"
|
|
5
5
|
|
|
6
6
|
module Gemvault
|
|
7
|
-
# Shared vault behavior: turn gem
|
|
8
|
-
# temporary files. Host classes must implement #gem_data(
|
|
9
|
-
#
|
|
7
|
+
# Shared vault behavior: turn a gem entry (stored or on disk) into gemspecs
|
|
8
|
+
# and temporary files. Host classes must implement #gem_data(entry) and
|
|
9
|
+
# #gem_entries.
|
|
10
10
|
module GemExtraction
|
|
11
|
-
def with_gem_file(
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
def with_gem_file(entry)
|
|
12
|
+
data = gem_data(entry)
|
|
13
|
+
Tempfile.create(["vault_gem", ".gem"]) do |tmpfile|
|
|
14
|
+
tmpfile.binmode
|
|
15
|
+
tmpfile.write(data)
|
|
16
|
+
tmpfile.flush
|
|
14
17
|
yield tmpfile.path
|
|
15
|
-
ensure
|
|
16
|
-
tmpfile.close unless tmpfile.closed?
|
|
17
|
-
tmpfile.unlink
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def spec_from_blob(
|
|
22
|
-
|
|
21
|
+
def spec_from_blob(entry)
|
|
22
|
+
bytes = gem_data(entry)
|
|
23
|
+
io = StringIO.new(bytes)
|
|
24
|
+
Gem::Package.new(io).spec
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def specs
|
|
26
|
-
gem_entries.map { |entry| spec_from_blob(entry
|
|
28
|
+
gem_entries.map { |entry| spec_from_blob(entry) }
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
private
|
|
30
32
|
|
|
31
33
|
def spec_from_gem_file(gem_path)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def write_tempfile(data)
|
|
38
|
-
tmpfile = Tempfile.new(["vault_gem", ".gem"])
|
|
39
|
-
tmpfile.binmode
|
|
40
|
-
tmpfile.write(data)
|
|
41
|
-
tmpfile.close
|
|
42
|
-
tmpfile
|
|
34
|
+
begin
|
|
35
|
+
Gem::Package.new(gem_path.to_s).spec
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
raise Vault::InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
|
|
38
|
+
end
|
|
43
39
|
end
|
|
44
40
|
end
|
|
45
41
|
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Gemvault
|
|
2
|
-
|
|
2
|
+
module GemReference
|
|
3
|
+
##
|
|
3
4
|
# Matches every version of a named gem stored in the vault.
|
|
4
|
-
class AnyVersion <
|
|
5
|
+
class AnyVersion < Data.define(:name)
|
|
6
|
+
include GemReference
|
|
7
|
+
|
|
8
|
+
def matches?(gem) = gem.name == name
|
|
5
9
|
end
|
|
6
10
|
end
|
|
7
11
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "rubygems/version"
|
|
2
|
+
|
|
3
|
+
module Gemvault
|
|
4
|
+
module GemReference
|
|
5
|
+
##
|
|
6
|
+
# Turns raw CLI input into the GemReference that selects it. Input is either
|
|
7
|
+
# a bare name ("foo") or a combined NAME-VERSION ("foo-1.2.3"); an explicit
|
|
8
|
+
# +version+ overrides any version embedded in the input. A name and version
|
|
9
|
+
# yield a SpecificVersion, a lone name an AnyVersion, and a non-exact version
|
|
10
|
+
# a NonExactVersionError.
|
|
11
|
+
class Parser < Data.define(:input, :version)
|
|
12
|
+
def initialize(input:, version: nil) = super
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
boundary = version_boundary
|
|
16
|
+
name = boundary ? input[0...boundary] : input
|
|
17
|
+
embedded_version = boundary && input[(boundary + 1)..]
|
|
18
|
+
build(name:, version_string: version || embedded_version)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build(name:, version_string:)
|
|
24
|
+
return AnyVersion.new(name:) if version_string.nil?
|
|
25
|
+
|
|
26
|
+
raise_non_exact(version_string) unless Gem::Version.correct?(version_string)
|
|
27
|
+
|
|
28
|
+
SpecificVersion.new(name:, version: Gem::Version.new(version_string))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def version_boundary
|
|
32
|
+
hyphen = input.rindex("-")
|
|
33
|
+
return nil unless hyphen
|
|
34
|
+
|
|
35
|
+
trailing = input[(hyphen + 1)..]
|
|
36
|
+
hyphen if Gem::Version.correct?(trailing)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def raise_non_exact(version_string)
|
|
40
|
+
raise NonExactVersionError, "Version must be an exact version (got: #{version_string.inspect})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
module Gemvault
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@version = version
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def ==(other)
|
|
15
|
-
super && version == other.version
|
|
16
|
-
end
|
|
17
|
-
alias eql? ==
|
|
18
|
-
|
|
19
|
-
def hash
|
|
20
|
-
[self.class, name, version].hash
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def deconstruct_keys(keys)
|
|
24
|
-
data = { name: name, version: version }
|
|
25
|
-
keys.nil? ? data : data.slice(*keys)
|
|
26
|
-
end
|
|
2
|
+
module GemReference
|
|
3
|
+
##
|
|
4
|
+
# Matches one exact version of a named gem. +version+ is always a
|
|
5
|
+
# Gem::Version (never nil, never a String); GemReference.parse validates and
|
|
6
|
+
# constructs it before calling .new.
|
|
7
|
+
class SpecificVersion < Data.define(:name, :version)
|
|
8
|
+
include GemReference
|
|
9
|
+
|
|
10
|
+
def matches?(gem) = gem.name == name && gem.version == version.to_s
|
|
27
11
|
end
|
|
28
12
|
end
|
|
29
13
|
end
|
|
@@ -1,54 +1,22 @@
|
|
|
1
1
|
require "rubygems/version"
|
|
2
|
-
require_relative "gem_reference/any_version"
|
|
3
|
-
require_relative "gem_reference/specific_version"
|
|
4
2
|
|
|
5
3
|
module Gemvault
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
4
|
+
##
|
|
5
|
+
# The role shared by every reference to a gem in a vault: it decides whether
|
|
6
|
+
# it +matches?+ a given GemEntry. Two value objects play the role --
|
|
7
|
+
# AnyVersion (no version constraint) and SpecificVersion (an exact
|
|
8
|
+
# Gem::Version). Players +include GemReference+ to declare the role; the
|
|
9
|
+
# +matches?+ contract is enforced by the "a gem reference" shared examples.
|
|
10
|
+
# +parse+ is the factory that turns raw CLI input into a player.
|
|
11
|
+
module GemReference
|
|
11
12
|
class NonExactVersionError < StandardError; end
|
|
12
13
|
|
|
13
14
|
def self.parse(input, version: nil)
|
|
14
|
-
|
|
15
|
-
version_string = version || embedded
|
|
16
|
-
return AnyVersion.new(name: base_name) if version_string.nil?
|
|
17
|
-
unless Gem::Version.correct?(version_string)
|
|
18
|
-
raise NonExactVersionError, "Version must be an exact version (got: #{version_string.inspect})"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
SpecificVersion.new(name: base_name, version: Gem::Version.new(version_string))
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.split_name_version(input)
|
|
25
|
-
idx = input.rindex("-")
|
|
26
|
-
return [input, nil] unless idx && Gem::Version.correct?(input[(idx + 1)..])
|
|
27
|
-
|
|
28
|
-
[input[0...idx], input[(idx + 1)..]]
|
|
29
|
-
end
|
|
30
|
-
private_class_method :split_name_version
|
|
31
|
-
|
|
32
|
-
attr_reader :name
|
|
33
|
-
|
|
34
|
-
def initialize(name:)
|
|
35
|
-
raise NotImplementedError, "abstract base use AnyVersion or SpecificVersion" if instance_of?(GemReference)
|
|
36
|
-
|
|
37
|
-
@name = name
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def ==(other)
|
|
41
|
-
self.class == other.class && name == other.name
|
|
42
|
-
end
|
|
43
|
-
alias eql? ==
|
|
44
|
-
|
|
45
|
-
def hash
|
|
46
|
-
[self.class, name].hash
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def deconstruct_keys(keys)
|
|
50
|
-
data = { name: name }
|
|
51
|
-
keys.nil? ? data : data.slice(*keys)
|
|
15
|
+
Parser.new(input:, version:).call
|
|
52
16
|
end
|
|
53
17
|
end
|
|
54
18
|
end
|
|
19
|
+
|
|
20
|
+
require_relative "gem_reference/parser"
|
|
21
|
+
require_relative "gem_reference/any_version"
|
|
22
|
+
require_relative "gem_reference/specific_version"
|
data/lib/gemvault/manifest.rb
CHANGED
|
@@ -1,76 +1,78 @@
|
|
|
1
1
|
require "json"
|
|
2
|
-
require "
|
|
2
|
+
require "digest"
|
|
3
3
|
require_relative "gem_entry"
|
|
4
4
|
|
|
5
5
|
module Gemvault
|
|
6
6
|
# The manifest.json stored as the first entry of a Tarvault. Records each
|
|
7
7
|
# gem's identity, timestamp, and SHA256 digest so listing and integrity
|
|
8
8
|
# checks never require reading every gem blob.
|
|
9
|
-
class Manifest
|
|
9
|
+
class Manifest < Data.define(:created_at, :records, :format_version)
|
|
10
10
|
FILENAME = "manifest.json".freeze
|
|
11
11
|
FORMAT_VERSION = 2
|
|
12
|
-
DIGEST = "SHA256".freeze
|
|
13
12
|
|
|
14
|
-
# One gem
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
# One stored gem: its identity (a GemEntry) plus the integrity digest and
|
|
14
|
+
# encryption flag the manifest keeps alongside it.
|
|
15
|
+
class StoredGem < Data.define(:gem, :sha256, :encrypted)
|
|
16
|
+
def self.from_h(hash)
|
|
17
|
+
hash => { name:, version:, platform:, created_at:, sha256:, encrypted: }
|
|
18
|
+
entry = GemEntry.new(name:, version:, platform:, created_at:)
|
|
19
|
+
new(gem: entry, sha256:, encrypted:)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def filename
|
|
21
|
-
|
|
22
|
+
def filename = gem.filename
|
|
23
|
+
|
|
24
|
+
def matches?(bytes) = Manifest.digest(bytes) == sha256
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
name: gem.name, version: gem.version, platform: gem.platform,
|
|
29
|
+
created_at: gem.created_at, sha256:, encrypted:
|
|
30
|
+
}
|
|
22
31
|
end
|
|
23
32
|
end
|
|
24
33
|
|
|
25
|
-
def self.digest(bytes)
|
|
26
|
-
OpenSSL::Digest.new(DIGEST).hexdigest(bytes)
|
|
27
|
-
end
|
|
34
|
+
def self.digest(bytes) = Digest::SHA256.hexdigest(bytes)
|
|
28
35
|
|
|
29
|
-
def self.empty(created_at:)
|
|
30
|
-
new(created_at: created_at, records: [])
|
|
31
|
-
end
|
|
36
|
+
def self.empty(created_at:) = new(created_at:, records: [])
|
|
32
37
|
|
|
33
38
|
def self.parse(json)
|
|
34
|
-
data = JSON.parse(json)
|
|
35
|
-
records = data.fetch(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
records: records,
|
|
39
|
-
format_version: (data["vault_version"] || FORMAT_VERSION).to_i,
|
|
40
|
-
)
|
|
39
|
+
data = JSON.parse(json, symbolize_names: true)
|
|
40
|
+
records = data.fetch(:gems, []).map { |gem| StoredGem.from_h(gem) }
|
|
41
|
+
format_version = (data[:vault_version] || FORMAT_VERSION).to_i
|
|
42
|
+
new(created_at: data[:created_at], records:, format_version:)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
attr_reader :records, :created_at, :format_version
|
|
44
|
-
|
|
45
45
|
def initialize(created_at:, records:, format_version: FORMAT_VERSION)
|
|
46
|
-
|
|
47
|
-
@records = records
|
|
48
|
-
@format_version = format_version
|
|
46
|
+
super
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
def with(record)
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
def with_record(record) = with(records: records + [record])
|
|
50
|
+
|
|
51
|
+
def without(dropped) = with(records: records - dropped)
|
|
54
52
|
|
|
55
|
-
def
|
|
56
|
-
|
|
53
|
+
def find(entry)
|
|
54
|
+
records.find { |record| record.gem.same_identity_as?(entry) }
|
|
57
55
|
end
|
|
58
56
|
|
|
59
|
-
def
|
|
60
|
-
records.
|
|
57
|
+
def matching(reference)
|
|
58
|
+
records.select { |record| reference.matches?(record.gem) }
|
|
61
59
|
end
|
|
62
60
|
|
|
61
|
+
def size = records.size
|
|
62
|
+
|
|
63
63
|
def gem_entries
|
|
64
|
-
records.sort_by { |
|
|
64
|
+
records.map(&:gem).sort_by { |gem| [gem.name, gem.version] }
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
def to_h
|
|
68
|
+
{
|
|
69
|
+
vault_version: FORMAT_VERSION,
|
|
70
|
+
format: "tarvault",
|
|
71
|
+
created_at:,
|
|
72
|
+
gems: records.map(&:to_h),
|
|
73
|
+
}
|
|
74
74
|
end
|
|
75
|
+
|
|
76
|
+
def to_json(*_args) = JSON.pretty_generate(to_h)
|
|
75
77
|
end
|
|
76
78
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "rubygems/package"
|
|
2
|
+
require "tempfile"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "archive_entry"
|
|
5
|
+
|
|
6
|
+
module Gemvault
|
|
7
|
+
# The tar file backing a Tarvault. Reads named members and rewrites the whole
|
|
8
|
+
# archive atomically: tar has no index and the leading manifest entry changes
|
|
9
|
+
# size on every mutation, so mutation means a full rewrite.
|
|
10
|
+
class Tarball
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = Pathname(path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read(name)
|
|
16
|
+
entries.find { |entry| entry.name == name }&.bytes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def entries
|
|
20
|
+
each_entry.map { |member| ArchiveEntry.new(name: member.full_name, bytes: member.read) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def write(entries)
|
|
24
|
+
Tempfile.create(["tarvault", ".tar"], @path.dirname) do |tmp|
|
|
25
|
+
write_entries(io: tmp, entries:)
|
|
26
|
+
tmp.flush
|
|
27
|
+
tmp.fsync
|
|
28
|
+
tmp.close
|
|
29
|
+
Pathname(tmp.path).rename(@path)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def each_entry(&block)
|
|
36
|
+
return enum_for(:each_entry) unless block
|
|
37
|
+
|
|
38
|
+
@path.open("rb") do |io|
|
|
39
|
+
Gem::Package::TarReader.new(io) { |reader| reader.each_entry(&block) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write_entries(io:, entries:)
|
|
44
|
+
Gem::Package::TarWriter.new(io) do |writer|
|
|
45
|
+
entries.each { |entry| add_entry(writer:, entry:) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add_entry(writer:, entry:)
|
|
50
|
+
writer.add_file_simple(entry.name, 0o644, entry.bytes.bytesize) { |dest| dest.write(entry.bytes) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/gemvault/tarvault.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require "time"
|
|
2
|
-
require "
|
|
2
|
+
require "pathname"
|
|
3
3
|
require_relative "vault"
|
|
4
4
|
require_relative "vault_session"
|
|
5
5
|
require_relative "gem_extraction"
|
|
6
6
|
require_relative "manifest"
|
|
7
|
-
require_relative "
|
|
7
|
+
require_relative "tarball"
|
|
8
|
+
require_relative "archive_entry"
|
|
9
|
+
require_relative "gem_entry"
|
|
8
10
|
require_relative "gem_reference"
|
|
9
11
|
|
|
10
12
|
module Gemvault
|
|
@@ -17,35 +19,40 @@ module Gemvault
|
|
|
17
19
|
attr_reader :path
|
|
18
20
|
|
|
19
21
|
def initialize(path, create: false)
|
|
20
|
-
@path =
|
|
21
|
-
@archive =
|
|
22
|
+
@path = Pathname(path).expand_path
|
|
23
|
+
@archive = Tarball.new(@path)
|
|
22
24
|
@closed = false
|
|
23
25
|
create ? create_vault! : load_manifest!
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def add(gem_path, created_at: nil)
|
|
27
|
-
gem_path =
|
|
28
|
-
raise Vault::NotFoundError, "Gem file not found: #{gem_path}" unless
|
|
29
|
+
gem_path = Pathname(gem_path).expand_path
|
|
30
|
+
raise Vault::NotFoundError, "Gem file not found: #{gem_path}" unless gem_path.file?
|
|
29
31
|
|
|
30
32
|
spec = spec_from_gem_file(gem_path)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
entry = GemEntry.from_spec(spec, created_at: created_at || timestamp)
|
|
34
|
+
raise_if_duplicate(entry)
|
|
35
|
+
store(entry:, bytes: gem_path.binread)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def remove(reference)
|
|
36
|
-
dropped =
|
|
39
|
+
dropped = @manifest.matching(reference)
|
|
37
40
|
return 0 if dropped.empty?
|
|
38
41
|
|
|
39
42
|
@manifest = @manifest.without(dropped)
|
|
40
|
-
|
|
43
|
+
remaining = survivors_excluding(dropped)
|
|
44
|
+
rewrite(remaining)
|
|
41
45
|
dropped.size
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
def gem_data(
|
|
45
|
-
record = @manifest.find(
|
|
46
|
-
raise Vault::NotFoundError, "Gem not found: #{
|
|
48
|
+
def gem_data(entry)
|
|
49
|
+
record = @manifest.find(entry)
|
|
50
|
+
raise Vault::NotFoundError, "Gem not found: #{entry}" unless record
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
bytes = @archive.read(record.filename)
|
|
53
|
+
raise Vault::Error, "Integrity check failed for #{record.filename}" unless record.matches?(bytes)
|
|
54
|
+
|
|
55
|
+
bytes
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def gem_entries
|
|
@@ -57,7 +64,7 @@ module Gemvault
|
|
|
57
64
|
end
|
|
58
65
|
|
|
59
66
|
def size
|
|
60
|
-
@manifest.
|
|
67
|
+
@manifest.size
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
def close
|
|
@@ -71,19 +78,21 @@ module Gemvault
|
|
|
71
78
|
private
|
|
72
79
|
|
|
73
80
|
def create_vault!
|
|
74
|
-
raise Vault::Error, "Vault already exists: #{@path}" if
|
|
81
|
+
raise Vault::Error, "Vault already exists: #{@path}" if @path.exist?
|
|
75
82
|
|
|
76
83
|
@manifest = Manifest.empty(created_at: timestamp)
|
|
77
84
|
rewrite([])
|
|
78
85
|
end
|
|
79
86
|
|
|
80
87
|
def load_manifest!
|
|
81
|
-
|
|
88
|
+
begin
|
|
89
|
+
raise Vault::NotFoundError, "Vault not found: #{@path}" unless @path.exist?
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
@manifest = Manifest.parse(read_manifest_json)
|
|
92
|
+
Vault.assert_readable!(version: @manifest.format_version, path: @path)
|
|
93
|
+
rescue JSON::ParserError, Gem::Package::TarInvalidError, ArgumentError, Errno::EINVAL
|
|
94
|
+
raise Vault::Error, "Not a valid Tarvault: #{@path}"
|
|
95
|
+
end
|
|
87
96
|
end
|
|
88
97
|
|
|
89
98
|
def read_manifest_json
|
|
@@ -93,52 +102,34 @@ module Gemvault
|
|
|
93
102
|
json
|
|
94
103
|
end
|
|
95
104
|
|
|
96
|
-
def store(
|
|
97
|
-
record = build_record(
|
|
98
|
-
@manifest = @manifest.
|
|
99
|
-
rewrite(
|
|
105
|
+
def store(entry:, bytes:)
|
|
106
|
+
record = build_record(entry:, bytes:)
|
|
107
|
+
@manifest = @manifest.with_record(record)
|
|
108
|
+
rewrite(survivors + [ArchiveEntry.new(name: record.filename, bytes:)])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def rewrite(gems)
|
|
112
|
+
manifest_entry = ArchiveEntry.new(name: Manifest::FILENAME, bytes: @manifest.to_json)
|
|
113
|
+
@archive.write([manifest_entry] + gems)
|
|
100
114
|
end
|
|
101
115
|
|
|
102
|
-
def
|
|
103
|
-
@archive.
|
|
116
|
+
def survivors
|
|
117
|
+
@archive.entries.reject { |entry| entry.name == Manifest::FILENAME }
|
|
104
118
|
end
|
|
105
119
|
|
|
106
120
|
def survivors_excluding(dropped)
|
|
107
121
|
names = dropped.map(&:filename)
|
|
108
|
-
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def build_record(spec, bytes, created_at)
|
|
112
|
-
Manifest::Record.new(
|
|
113
|
-
name: spec.name,
|
|
114
|
-
version: spec.version.to_s,
|
|
115
|
-
platform: spec.platform.to_s,
|
|
116
|
-
created_at: created_at,
|
|
117
|
-
sha256: Manifest.digest(bytes),
|
|
118
|
-
encrypted: false,
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def matching_records(reference)
|
|
123
|
-
case reference
|
|
124
|
-
in GemReference::AnyVersion[name:]
|
|
125
|
-
@manifest.records.select { |r| r.name == name }
|
|
126
|
-
in GemReference::SpecificVersion[name:, version:]
|
|
127
|
-
@manifest.records.select { |r| r.name == name && r.version == version.to_s }
|
|
128
|
-
end
|
|
122
|
+
survivors.reject { |entry| names.include?(entry.name) }
|
|
129
123
|
end
|
|
130
124
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
raise Vault::DuplicateGemError,
|
|
135
|
-
"Gem already in vault: #{spec.name}-#{spec.version} (#{spec.platform})"
|
|
125
|
+
def build_record(entry:, bytes:)
|
|
126
|
+
Manifest::StoredGem.new(gem: entry, sha256: Manifest.digest(bytes), encrypted: false)
|
|
136
127
|
end
|
|
137
128
|
|
|
138
|
-
def
|
|
139
|
-
return
|
|
129
|
+
def raise_if_duplicate(entry)
|
|
130
|
+
return unless @manifest.find(entry)
|
|
140
131
|
|
|
141
|
-
raise Vault::
|
|
132
|
+
raise Vault::DuplicateGemError, "Gem already in vault: #{entry}"
|
|
142
133
|
end
|
|
143
134
|
|
|
144
135
|
def timestamp
|
data/lib/gemvault/vault.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "forwardable"
|
|
2
|
+
require "pathname"
|
|
2
3
|
require_relative "vault_session"
|
|
3
4
|
|
|
4
5
|
module Gemvault
|
|
@@ -29,7 +30,7 @@ module Gemvault
|
|
|
29
30
|
:spec_from_blob, :with_gem_file, :size, :close, :closed?,
|
|
30
31
|
:path, :format_version
|
|
31
32
|
|
|
32
|
-
def self.assert_readable!(version
|
|
33
|
+
def self.assert_readable!(version:, path:)
|
|
33
34
|
return if version.between?(MIN_READABLE_FORMAT, CURRENT_FORMAT)
|
|
34
35
|
|
|
35
36
|
raise UnsupportedVersionError,
|
|
@@ -38,7 +39,7 @@ module Gemvault
|
|
|
38
39
|
|
|
39
40
|
def self.backend_for(path, create:)
|
|
40
41
|
return build_tarvault(path, create: true) if create
|
|
41
|
-
raise NotFoundError, "Vault not found: #{path}" unless
|
|
42
|
+
raise NotFoundError, "Vault not found: #{path}" unless path.exist?
|
|
42
43
|
|
|
43
44
|
case container_kind(path)
|
|
44
45
|
when :sqlite then build_dbvault(path)
|
|
@@ -56,32 +57,35 @@ module Gemvault
|
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def self.sqlite?(path)
|
|
59
|
-
|
|
60
|
+
path.exist? && path.binread(SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
def self.tar?(path)
|
|
63
|
-
return false unless
|
|
64
|
+
return false unless path.exist?
|
|
64
65
|
|
|
65
|
-
header =
|
|
66
|
+
header = path.binread(TAR_MAGIC_OFFSET + TAR_MAGIC.bytesize)
|
|
66
67
|
header.to_s[TAR_MAGIC_OFFSET, TAR_MAGIC.bytesize] == TAR_MAGIC
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def self.build_dbvault(path)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
begin
|
|
72
|
+
require_relative "dbvault"
|
|
73
|
+
Dbvault.new(path)
|
|
74
|
+
rescue LoadError => e
|
|
75
|
+
raise Error,
|
|
76
|
+
"#{path} is a legacy SQLite vault; it needs the sqlite3 gem (#{e.message}). " \
|
|
77
|
+
"Install sqlite3, or upgrade the vault with a gemvault that includes it."
|
|
78
|
+
end
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def self.build_tarvault(path, create:)
|
|
79
82
|
require_relative "tarvault"
|
|
80
|
-
Tarvault.new(path, create:
|
|
83
|
+
Tarvault.new(path, create:)
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
def initialize(path, create: false)
|
|
84
|
-
|
|
87
|
+
absolute_path = Pathname(path).expand_path
|
|
88
|
+
@backend = self.class.backend_for(absolute_path, create:)
|
|
85
89
|
end
|
|
86
90
|
end
|
|
87
91
|
end
|