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.
@@ -4,42 +4,38 @@ require "stringio"
4
4
  require_relative "vault"
5
5
 
6
6
  module Gemvault
7
- # Shared vault behavior: turn gem bytes (stored or on disk) into gemspecs and
8
- # temporary files. Host classes must implement #gem_data(name, version,
9
- # platform:) and #gem_entries.
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(name, version, platform: "ruby")
12
- tmpfile = write_tempfile(gem_data(name, version, platform: platform))
13
- begin
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(name, version, platform = "ruby")
22
- Gem::Package.new(StringIO.new(gem_data(name, version, platform: platform))).spec
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.name, entry.version, entry.platform) }
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
- Gem::Package.new(gem_path).spec
33
- rescue StandardError => e
34
- raise Vault::InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
35
- end
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
- class GemReference
2
+ module GemReference
3
+ ##
3
4
  # Matches every version of a named gem stored in the vault.
4
- class AnyVersion < GemReference
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
- class GemReference
3
- # Matches one exact version of a named gem. `version:` is always a
4
- # Gem::Version (never nil, never a String) `.parse` validates and
5
- # constructs it before calling `.new`.
6
- class SpecificVersion < GemReference
7
- attr_reader :version
8
-
9
- def initialize(name:, version:)
10
- super(name: name)
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
- # A reference to a gem in a vault. Abstract base class for two concrete
7
- # kinds: AnyVersion (no version constraint) and SpecificVersion (an exact
8
- # Gem::Version). Not instantiable directly. `.parse` is the factory that
9
- # turns raw CLI input into one of the two subclasses.
10
- class GemReference
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
- base_name, embedded = split_name_version(input)
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"
@@ -1,76 +1,78 @@
1
1
  require "json"
2
- require "openssl"
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's metadata inside a manifest.
15
- Record = Struct.new(:name, :version, :platform, :created_at, :sha256, :encrypted, keyword_init: true) do
16
- def to_gem_entry
17
- GemEntry.new(name: name, version: version, platform: platform, created_at: created_at)
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
- to_gem_entry.filename
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("gems", []).map { |g| Record.new(**g.transform_keys(&:to_sym)) }
36
- new(
37
- created_at: data["created_at"],
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
- @created_at = created_at
47
- @records = records
48
- @format_version = format_version
46
+ super
49
47
  end
50
48
 
51
- def with(record)
52
- self.class.new(created_at: created_at, records: records + [record], format_version: format_version)
53
- end
49
+ def with_record(record) = with(records: records + [record])
50
+
51
+ def without(dropped) = with(records: records - dropped)
54
52
 
55
- def without(dropped)
56
- self.class.new(created_at: created_at, records: records - dropped, format_version: format_version)
53
+ def find(entry)
54
+ records.find { |record| record.gem.same_identity_as?(entry) }
57
55
  end
58
56
 
59
- def find(name, version, platform)
60
- records.find { |r| r.name == name && r.version == version && r.platform == platform }
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 { |r| [r.name, r.version] }.map(&:to_gem_entry)
64
+ records.map(&:gem).sort_by { |gem| [gem.name, gem.version] }
65
65
  end
66
66
 
67
- def to_json(*_args)
68
- JSON.pretty_generate(
69
- "vault_version" => FORMAT_VERSION,
70
- "format" => "tarvault",
71
- "created_at" => created_at,
72
- "gems" => records.map(&:to_h),
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
@@ -1,10 +1,12 @@
1
1
  require "time"
2
- require "json"
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 "tar_archive"
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 = File.expand_path(path)
21
- @archive = TarArchive.new(@path)
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 = File.expand_path(gem_path)
28
- raise Vault::NotFoundError, "Gem file not found: #{gem_path}" unless File.file?(gem_path)
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
- raise_if_duplicate(spec)
32
- store(spec, File.binread(gem_path), created_at || timestamp)
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 = matching_records(reference)
39
+ dropped = @manifest.matching(reference)
37
40
  return 0 if dropped.empty?
38
41
 
39
42
  @manifest = @manifest.without(dropped)
40
- rewrite(survivors_excluding(dropped))
43
+ remaining = survivors_excluding(dropped)
44
+ rewrite(remaining)
41
45
  dropped.size
42
46
  end
43
47
 
44
- def gem_data(name, version, platform: "ruby")
45
- record = @manifest.find(name, version, platform)
46
- raise Vault::NotFoundError, "Gem not found: #{name}-#{version} (#{platform})" unless record
48
+ def gem_data(entry)
49
+ record = @manifest.find(entry)
50
+ raise Vault::NotFoundError, "Gem not found: #{entry}" unless record
47
51
 
48
- verify(@archive.read(record.filename), record)
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.records.size
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 File.exist?(@path)
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
- raise Vault::NotFoundError, "Vault not found: #{@path}" unless File.exist?(@path)
88
+ begin
89
+ raise Vault::NotFoundError, "Vault not found: #{@path}" unless @path.exist?
82
90
 
83
- @manifest = Manifest.parse(read_manifest_json)
84
- Vault.assert_readable!(@manifest.format_version, @path)
85
- rescue JSON::ParserError, Gem::Package::TarInvalidError, ArgumentError, Errno::EINVAL
86
- raise Vault::Error, "Not a valid Tarvault: #{@path}"
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(spec, bytes, created_at)
97
- record = build_record(spec, bytes, created_at)
98
- @manifest = @manifest.with(record)
99
- rewrite(@archive.gem_pairs + [[record.filename, bytes]])
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 rewrite(gem_pairs)
103
- @archive.write([[Manifest::FILENAME, @manifest.to_json]] + gem_pairs)
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
- @archive.gem_pairs.reject { |pair| names.include?(pair.first) }
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 raise_if_duplicate(spec)
132
- return unless @manifest.find(spec.name, spec.version.to_s, spec.platform.to_s)
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 verify(bytes, record)
139
- return bytes if Manifest.digest(bytes) == record.sha256
129
+ def raise_if_duplicate(entry)
130
+ return unless @manifest.find(entry)
140
131
 
141
- raise Vault::Error, "Integrity check failed for #{record.filename}"
132
+ raise Vault::DuplicateGemError, "Gem already in vault: #{entry}"
142
133
  end
143
134
 
144
135
  def timestamp
@@ -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, path)
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 File.exist?(path)
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
- File.exist?(path) && File.binread(path, SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
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 File.exist?(path)
64
+ return false unless path.exist?
64
65
 
65
- header = File.binread(path, TAR_MAGIC_OFFSET + TAR_MAGIC.bytesize)
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
- require_relative "dbvault"
71
- Dbvault.new(path)
72
- rescue LoadError => e
73
- raise Error,
74
- "#{path} is a legacy SQLite vault; it needs the sqlite3 gem (#{e.message}). " \
75
- "Install sqlite3, or upgrade the vault with a gemvault that includes it."
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: create)
83
+ Tarvault.new(path, create:)
81
84
  end
82
85
 
83
86
  def initialize(path, create: false)
84
- @backend = self.class.backend_for(File.expand_path(path), create: create)
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