gemvault 0.1.4 → 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 +56 -0
- data/CLAUDE.md +6 -5
- data/Dockerfile.test +14 -0
- data/README.md +18 -6
- data/Rakefile +19 -0
- data/docs/tarvault-findings.md +186 -0
- data/docs/tarvault.md +42 -0
- data/issues.rec +540 -17
- data/lib/bundler/plugin/vault_source.rb +61 -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 +72 -0
- data/lib/gemvault/dbvault.rb +101 -0
- data/lib/gemvault/deprecation.rb +48 -0
- data/lib/gemvault/gem_entry.rb +20 -24
- data/lib/gemvault/gem_extraction.rb +41 -0
- 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 +78 -0
- data/lib/gemvault/tarball.rb +53 -0
- data/lib/gemvault/tarvault.rb +139 -0
- data/lib/gemvault/vault.rb +55 -157
- data/lib/gemvault/vault_path.rb +33 -0
- data/lib/gemvault/vault_session.rb +16 -0
- data/lib/gemvault/vault_upgrade.rb +94 -0
- data/lib/gemvault/version.rb +1 -1
- data/lib/gemvault.rb +2 -0
- data/lib/rubygems/resolver/vault_set.rb +5 -7
- data/lib/rubygems/source/vault.rb +28 -32
- data/lib/rubygems_plugin.rb +17 -6
- metadata +21 -33
|
@@ -1,39 +1,71 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "pathname"
|
|
2
|
+
require_relative "vaulted_gem"
|
|
2
3
|
require_relative "../../gemvault/vault"
|
|
4
|
+
require_relative "../../gemvault/gem_entry"
|
|
3
5
|
|
|
4
6
|
module Bundler
|
|
5
7
|
module Plugin
|
|
6
|
-
# Bundler plugin source that installs gems from a .gemv vault file.
|
|
8
|
+
# Bundler plugin source that installs gems from a .gemv vault file. A thin
|
|
9
|
+
# doorway over the Bundler Source API: it resolves the vault path, hands
|
|
10
|
+
# Bundler the gemspecs it can install, and delegates each gem's placement in
|
|
11
|
+
# the bundle to a VaultedGem.
|
|
7
12
|
class VaultSource
|
|
8
13
|
def initialize(opts)
|
|
9
14
|
super
|
|
10
|
-
@vault_path =
|
|
15
|
+
@vault_path = Pathname(@uri).expand_path(Bundler.root)
|
|
16
|
+
@allow_remote = false
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def fetch_gemspec_files
|
|
14
20
|
validate_vault_exists!
|
|
15
21
|
|
|
16
22
|
Gemvault::Vault.open(@vault_path) do |vault|
|
|
17
|
-
vault.gem_entries
|
|
23
|
+
vault.gem_entries
|
|
24
|
+
.map { |entry| vault.spec_from_blob(entry) }
|
|
25
|
+
.filter_map { |spec| gemspec_file_for(spec) }
|
|
18
26
|
end
|
|
19
27
|
end
|
|
20
28
|
|
|
29
|
+
# Bundler first asks a source, in local mode, which gems are already
|
|
30
|
+
# unpacked so it can tell what still needs installing, then switches to
|
|
31
|
+
# remote mode to resolve and install the rest. A vault gem is not on the
|
|
32
|
+
# load path until it is unpacked, so advertising one that still lives only
|
|
33
|
+
# inside the archive makes Bundler believe it is installed: it skips the
|
|
34
|
+
# install and the later require fails. Advertise not-yet-unpacked gems
|
|
35
|
+
# only once Bundler has asked for remote specs.
|
|
36
|
+
def remote!
|
|
37
|
+
@allow_remote = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The inverse of remote!. Bundler's local-only resolve (Definition#check!,
|
|
41
|
+
# reached by `bundle check`) calls local_only! on every source. The base
|
|
42
|
+
# Bundler::Plugin::API::Source defines no such hook, so without this the
|
|
43
|
+
# command dies with NoMethodError; here it simply stops advertising gems
|
|
44
|
+
# that live only inside the archive.
|
|
45
|
+
def local_only!
|
|
46
|
+
@allow_remote = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Also invoked on every source during resolution (Definition#prefer_local!,
|
|
50
|
+
# for `--prefer-local`). The vault reads from one local file already, so
|
|
51
|
+
# there is no remote to deprioritize.
|
|
52
|
+
def prefer_local!; end
|
|
53
|
+
|
|
21
54
|
def install(spec, opts = {})
|
|
22
|
-
|
|
23
|
-
return
|
|
55
|
+
gem = VaultedGem.new(spec)
|
|
56
|
+
return gem.adopt if gem.installed? && !opts[:force]
|
|
24
57
|
|
|
25
|
-
Bundler.ui.confirm "Installing #{version_message
|
|
26
|
-
|
|
27
|
-
spec.post_install_message
|
|
58
|
+
Bundler.ui.confirm "Installing #{gem.version_message} from vault #{@uri}"
|
|
59
|
+
extract(spec) { |gem_path| gem.install(gem_path:, build_args: opts[:build_args] || []) }
|
|
28
60
|
end
|
|
29
61
|
|
|
30
62
|
def options_to_lock
|
|
31
63
|
{}
|
|
32
64
|
end
|
|
33
65
|
|
|
34
|
-
# No source-level install_path to copy: VaultSource#install installs
|
|
35
|
-
#
|
|
36
|
-
#
|
|
66
|
+
# No source-level install_path to copy: VaultSource#install installs each
|
|
67
|
+
# gem into Bundler.bundle_path via RubyGemsGemInstaller, so the default
|
|
68
|
+
# Source#cache would dereference a non-existent directory.
|
|
37
69
|
def cache(spec, custom_path = nil); end
|
|
38
70
|
|
|
39
71
|
def to_s
|
|
@@ -42,79 +74,35 @@ module Bundler
|
|
|
42
74
|
|
|
43
75
|
private
|
|
44
76
|
|
|
45
|
-
def gemspec_file_for(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
write_tmp_gemspec(spec.full_name, spec.to_ruby)
|
|
51
|
-
end
|
|
77
|
+
def gemspec_file_for(spec)
|
|
78
|
+
gem = VaultedGem.new(spec)
|
|
79
|
+
return gem.anchor(spec.to_ruby) if gem.installed?
|
|
80
|
+
return nil unless @allow_remote
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
gemspec_dir = File.join(Bundler.tmp("vault_source"), "specifications")
|
|
55
|
-
FileUtils.mkdir_p(gemspec_dir)
|
|
56
|
-
gemspec_path = File.join(gemspec_dir, "#{full_name}.gemspec")
|
|
57
|
-
File.write(gemspec_path, spec_ruby)
|
|
58
|
-
gemspec_path
|
|
82
|
+
write_tmp_gemspec(spec)
|
|
59
83
|
end
|
|
60
84
|
|
|
61
|
-
def
|
|
62
|
-
Bundler.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
def write_tmp_gemspec(spec)
|
|
86
|
+
tmp_dir = Pathname(Bundler.tmp("vault_source"))
|
|
87
|
+
gemspec_dir = tmp_dir / "specifications"
|
|
88
|
+
gemspec_dir.mkpath
|
|
89
|
+
gemspec = gemspec_dir / "#{spec.full_name}.gemspec"
|
|
90
|
+
gemspec.write(spec.to_ruby)
|
|
91
|
+
gemspec.to_s
|
|
66
92
|
end
|
|
67
93
|
|
|
68
|
-
def
|
|
94
|
+
def extract(spec, &)
|
|
69
95
|
Gemvault::Vault.open(@vault_path) do |vault|
|
|
70
|
-
vault.with_gem_file(spec
|
|
71
|
-
installed_spec = build_installer(gem_path, opts).install
|
|
72
|
-
gem_dir = installed_spec.full_gem_path
|
|
73
|
-
spec.full_gem_path = gem_dir
|
|
74
|
-
spec.loaded_from = anchor_gemspec(gem_dir, spec.full_name, installed_spec.to_ruby)
|
|
75
|
-
end
|
|
96
|
+
vault.with_gem_file(entry_for(spec), &)
|
|
76
97
|
end
|
|
77
98
|
end
|
|
78
99
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Bundler::RubyGemsGemInstaller.at(
|
|
83
|
-
gem_path,
|
|
84
|
-
install_dir: Bundler.bundle_path.to_s,
|
|
85
|
-
bin_dir: Bundler.system_bindir.to_s,
|
|
86
|
-
ignore_dependencies: true,
|
|
87
|
-
wrappers: true,
|
|
88
|
-
env_shebang: true,
|
|
89
|
-
build_args: opts[:build_args] || [],
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def version_message(spec)
|
|
94
|
-
message = "#{spec.name} #{spec.version}"
|
|
95
|
-
message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil?
|
|
96
|
-
message
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def resolve_vault_path(uri)
|
|
100
|
-
File.expand_path(uri, Bundler.root.to_s)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Bundler computes full_gem_path as dirname(loaded_from) for plugin
|
|
104
|
-
# sources, so the gemspec must live inside the gem directory -- not
|
|
105
|
-
# in specifications/ -- for load paths to resolve correctly.
|
|
106
|
-
def anchor_gemspec(gem_dir, full_name, spec_ruby)
|
|
107
|
-
gemspec_path = File.join(gem_dir, "#{full_name}.gemspec")
|
|
108
|
-
File.write(gemspec_path, spec_ruby) unless File.exist?(gemspec_path)
|
|
109
|
-
gemspec_path
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def gem_dir_for(full_name)
|
|
113
|
-
File.join(Bundler.bundle_path, "gems", full_name)
|
|
100
|
+
def entry_for(spec)
|
|
101
|
+
Gemvault::GemEntry.from_spec(spec)
|
|
114
102
|
end
|
|
115
103
|
|
|
116
104
|
def validate_vault_exists!
|
|
117
|
-
return if
|
|
105
|
+
return if @vault_path.file?
|
|
118
106
|
|
|
119
107
|
raise Bundler::PathError,
|
|
120
108
|
"Could not find vault '#{@uri}' referenced in Gemfile " \
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Plugin
|
|
5
|
+
# A gem sourced from a vault as it lives (or will live) inside the bundle.
|
|
6
|
+
# Owns where the gem unpacks, whether it is already installed, installing an
|
|
7
|
+
# extracted .gem file, and anchoring its gemspec inside the gem directory --
|
|
8
|
+
# Bundler derives full_gem_path from dirname(loaded_from), so the gemspec
|
|
9
|
+
# must sit beside the gem, not in specifications/.
|
|
10
|
+
class VaultedGem
|
|
11
|
+
def initialize(spec)
|
|
12
|
+
@spec = spec
|
|
13
|
+
@gem_dir = Bundler.bundle_path.join("gems", spec.full_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def installed?
|
|
17
|
+
@gem_dir.directory?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def version_message
|
|
21
|
+
message = "#{@spec.name} #{@spec.version}"
|
|
22
|
+
message += " (#{@spec.platform})" if @spec.platform != Gem::Platform::RUBY && !@spec.platform.nil?
|
|
23
|
+
message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Point the Bundler spec at the already-unpacked gem.
|
|
27
|
+
def adopt
|
|
28
|
+
Bundler.ui.debug "Using #{version_message} from vault"
|
|
29
|
+
@spec.full_gem_path = @gem_dir.to_s
|
|
30
|
+
@spec.loaded_from = anchor(@spec.to_ruby)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Install the extracted .gem file into the bundle, point the spec at it,
|
|
35
|
+
# and return its post-install message.
|
|
36
|
+
def install(gem_path:, build_args:)
|
|
37
|
+
installed = installer(gem_path:, build_args:).install
|
|
38
|
+
@spec.full_gem_path = installed.full_gem_path
|
|
39
|
+
@spec.loaded_from = anchor(installed.to_ruby, dir: installed.full_gem_path)
|
|
40
|
+
@spec.post_install_message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def anchor(spec_ruby, dir: @gem_dir)
|
|
44
|
+
gemspec = Pathname(dir).join("#{@spec.full_name}.gemspec")
|
|
45
|
+
gemspec.write(spec_ruby) unless gemspec.exist?
|
|
46
|
+
gemspec.to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def installer(gem_path:, build_args:)
|
|
52
|
+
require "bundler/rubygems_gem_installer"
|
|
53
|
+
|
|
54
|
+
Bundler::RubyGemsGemInstaller.at(
|
|
55
|
+
gem_path,
|
|
56
|
+
install_dir: Bundler.bundle_path.to_s,
|
|
57
|
+
bin_dir: Bundler.system_bindir.to_s,
|
|
58
|
+
ignore_dependencies: true,
|
|
59
|
+
wrappers: true,
|
|
60
|
+
env_shebang: true,
|
|
61
|
+
build_args:,
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/gemvault/cli/command.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "command_kit/command"
|
|
2
2
|
require_relative "../vault"
|
|
3
|
+
require_relative "../vault_path"
|
|
3
4
|
|
|
4
5
|
module Gemvault
|
|
5
6
|
class CLI
|
|
@@ -7,11 +8,14 @@ module Gemvault
|
|
|
7
8
|
class Command < CommandKit::Command
|
|
8
9
|
private
|
|
9
10
|
|
|
10
|
-
def with_vault(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
def with_vault(locator, create: false, &block)
|
|
12
|
+
begin
|
|
13
|
+
path = VaultPath.resolve(locator)
|
|
14
|
+
Gemvault::Vault.open(path, create:, &block)
|
|
15
|
+
rescue Gemvault::Vault::Error => e
|
|
16
|
+
print_error(e.message)
|
|
17
|
+
exit(1)
|
|
18
|
+
end
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "pathname"
|
|
2
2
|
require_relative "../command"
|
|
3
3
|
|
|
4
4
|
module Gemvault
|
|
@@ -25,21 +25,21 @@ module Gemvault
|
|
|
25
25
|
desc: "Output directory"
|
|
26
26
|
|
|
27
27
|
def run(vault, name, version = nil)
|
|
28
|
-
output_dir = options[:output]
|
|
28
|
+
output_dir = Pathname(options[:output])
|
|
29
29
|
|
|
30
30
|
with_vault(vault) do |v|
|
|
31
|
-
|
|
32
|
-
entries = matching_entries(v, name
|
|
31
|
+
output_dir.mkpath
|
|
32
|
+
entries = matching_entries(vault: v, name:, version:)
|
|
33
33
|
abort_missing_gem(name) if entries.empty?
|
|
34
|
-
extract_entries(v, entries
|
|
34
|
+
extract_entries(vault: v, entries:, output_dir:)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
39
39
|
|
|
40
|
-
def matching_entries(vault
|
|
41
|
-
entries = vault.gem_entries.select { |
|
|
42
|
-
version ? entries.select { |
|
|
40
|
+
def matching_entries(vault:, name:, version:)
|
|
41
|
+
entries = vault.gem_entries.select { |entry| entry.name == name }
|
|
42
|
+
version ? entries.select { |entry| entry.version == version } : entries
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def abort_missing_gem(name)
|
|
@@ -47,10 +47,10 @@ module Gemvault
|
|
|
47
47
|
exit(1)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def extract_entries(vault
|
|
50
|
+
def extract_entries(vault:, entries:, output_dir:)
|
|
51
51
|
entries.each do |entry|
|
|
52
|
-
data = vault.gem_data(entry
|
|
53
|
-
|
|
52
|
+
data = vault.gem_data(entry)
|
|
53
|
+
(output_dir / entry.filename).binwrite(data)
|
|
54
54
|
puts "Extracted #{entry.filename}"
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "pathname"
|
|
1
2
|
require_relative "../command"
|
|
2
3
|
|
|
3
4
|
module Gemvault
|
|
@@ -12,9 +13,9 @@ module Gemvault
|
|
|
12
13
|
desc: "Vault name (auto-appends .gemv)"
|
|
13
14
|
|
|
14
15
|
def run(name)
|
|
15
|
-
path = name.end_with?(".gemv") ? name : "#{name}.gemv"
|
|
16
|
+
path = Pathname(name.end_with?(".gemv") ? name : "#{name}.gemv")
|
|
16
17
|
|
|
17
|
-
if
|
|
18
|
+
if path.exist?
|
|
18
19
|
print_error("#{path} already exists")
|
|
19
20
|
exit(1)
|
|
20
21
|
end
|
|
@@ -26,15 +26,25 @@ module Gemvault
|
|
|
26
26
|
desc: "Gem version (overrides positional and NAME-VERSION forms)"
|
|
27
27
|
|
|
28
28
|
def run(vault, name, positional_version = nil)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
begin
|
|
30
|
+
version = options[:version] || positional_version
|
|
31
|
+
reference = Gemvault::GemReference.parse(name, version:)
|
|
32
|
+
remove_matching(vault:, reference:)
|
|
33
|
+
rescue Gemvault::GemReference::NonExactVersionError => e
|
|
34
|
+
print_error(e.message)
|
|
35
|
+
exit(1)
|
|
36
|
+
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
private
|
|
37
40
|
|
|
41
|
+
def remove_matching(vault:, reference:)
|
|
42
|
+
with_vault(vault) do |v|
|
|
43
|
+
removed = v.remove(reference)
|
|
44
|
+
report_removal(removed)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
38
48
|
def report_removal(count)
|
|
39
49
|
if count.zero?
|
|
40
50
|
print_error("No matching gem found")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require_relative "../command"
|
|
2
|
+
require_relative "../../vault_path"
|
|
3
|
+
require_relative "../../vault_upgrade"
|
|
4
|
+
|
|
5
|
+
module Gemvault
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
# Upgrades a vault to the current storage format (e.g. a SQLite Dbvault
|
|
9
|
+
# to a Tarvault), preserving every gem and its timestamp.
|
|
10
|
+
class Upgrade < Command
|
|
11
|
+
description "Upgrade a vault to the current storage format"
|
|
12
|
+
|
|
13
|
+
argument :vault, required: true,
|
|
14
|
+
usage: "VAULT",
|
|
15
|
+
desc: "Vault file"
|
|
16
|
+
|
|
17
|
+
option :dry_run, desc: "Show what would change without modifying the vault"
|
|
18
|
+
option :no_backup, desc: "Do not write a .bak copy before upgrading"
|
|
19
|
+
|
|
20
|
+
def run(vault)
|
|
21
|
+
begin
|
|
22
|
+
upgrade = build_upgrade(vault)
|
|
23
|
+
dispatch(vault:, upgrade:)
|
|
24
|
+
rescue Gemvault::Vault::Error => e
|
|
25
|
+
print_error(e.message)
|
|
26
|
+
exit(1)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_upgrade(vault)
|
|
33
|
+
path = VaultPath.resolve(vault)
|
|
34
|
+
Gemvault::VaultUpgrade.new(path, backup: backup?)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def backup?
|
|
38
|
+
!options[:no_backup]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dispatch(vault:, upgrade:)
|
|
42
|
+
summary = upgrade.plan
|
|
43
|
+
case decision(summary)
|
|
44
|
+
in { no_op: true }
|
|
45
|
+
report_no_op(vault:, summary:)
|
|
46
|
+
in { dry_run: true }
|
|
47
|
+
report_dry_run(vault:, summary:)
|
|
48
|
+
else
|
|
49
|
+
upgrade.call
|
|
50
|
+
report_upgraded(vault:, summary:)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def decision(summary)
|
|
55
|
+
{ no_op: summary.no_op?, dry_run: options[:dry_run] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def report_no_op(vault:, summary:)
|
|
59
|
+
puts "#{vault} is already current (format #{summary.to_version})"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def report_dry_run(vault:, summary:)
|
|
63
|
+
puts "Would upgrade #{vault}: #{summary} (#{summary.gem_count} gems)"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def report_upgraded(vault:, summary:)
|
|
67
|
+
puts "Upgraded #{vault}: #{summary} (#{summary.gem_count} gems)"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "sqlite3"
|
|
2
|
+
require "pathname"
|
|
3
|
+
require_relative "vault"
|
|
4
|
+
require_relative "vault_session"
|
|
5
|
+
require_relative "gem_extraction"
|
|
6
|
+
require_relative "gem_entry"
|
|
7
|
+
require_relative "deprecation"
|
|
8
|
+
|
|
9
|
+
module Gemvault
|
|
10
|
+
# Read-only reader for a SQLite-backed vault (a "Dbvault"). Attempts to write
|
|
11
|
+
# are refused with a pointer to run gemvault upgrade, and opening a vault
|
|
12
|
+
# emits a one-time notice.
|
|
13
|
+
class Dbvault
|
|
14
|
+
extend VaultSession
|
|
15
|
+
include GemExtraction
|
|
16
|
+
|
|
17
|
+
FORMAT_VERSION = 1
|
|
18
|
+
|
|
19
|
+
attr_reader :path
|
|
20
|
+
|
|
21
|
+
def initialize(path)
|
|
22
|
+
@path = Pathname(path).expand_path
|
|
23
|
+
open_vault!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add(*)
|
|
27
|
+
raise Vault::ReadOnlyError, read_only_message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def remove(*)
|
|
31
|
+
raise Vault::ReadOnlyError, read_only_message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def gem_data(entry)
|
|
35
|
+
row = @db.execute(
|
|
36
|
+
"SELECT data FROM gems WHERE name = ? AND version = ? AND platform = ?",
|
|
37
|
+
[entry.name, entry.version, entry.platform],
|
|
38
|
+
).first
|
|
39
|
+
raise Vault::NotFoundError, "Gem not found: #{entry}" unless row
|
|
40
|
+
|
|
41
|
+
row["data"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def gem_entries
|
|
45
|
+
@db.execute(
|
|
46
|
+
"SELECT name, version, platform, created_at FROM gems ORDER BY name, version",
|
|
47
|
+
).map { |row| GemEntry.new(**row.transform_keys(&:to_sym)) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def size
|
|
51
|
+
@db.execute("SELECT COUNT(*) AS count FROM gems").first["count"]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def close
|
|
55
|
+
@db.close if @db && !@db.closed?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def closed?
|
|
59
|
+
@db.nil? || @db.closed?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def format_version
|
|
63
|
+
row = @db.execute("SELECT value FROM metadata WHERE key = 'vault_version'").first
|
|
64
|
+
return FORMAT_VERSION unless row
|
|
65
|
+
|
|
66
|
+
row["value"].to_i
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def open_vault!
|
|
72
|
+
raise Vault::NotFoundError, "Vault not found: #{@path}" unless @path.exist?
|
|
73
|
+
|
|
74
|
+
validate_sqlite!
|
|
75
|
+
@db = new_database
|
|
76
|
+
Vault.assert_readable!(version: format_version, path: @path)
|
|
77
|
+
Deprecation.warn_once(deprecation_message)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def new_database
|
|
81
|
+
db = SQLite3::Database.new(@path.to_s)
|
|
82
|
+
db.results_as_hash = true
|
|
83
|
+
db
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validate_sqlite!
|
|
87
|
+
return if @path.binread(Vault::SQLITE_MAGIC.bytesize) == Vault::SQLITE_MAGIC
|
|
88
|
+
|
|
89
|
+
raise Vault::Error, "Not a valid vault file (not SQLite): #{@path}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read_only_message
|
|
93
|
+
"#{@path} uses the deprecated read-only SQLite format. Migrate it first: gemvault upgrade #{@path}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def deprecation_message
|
|
97
|
+
"SQLite vaults are deprecated and read-only; support will be removed in a future release (0.3-0.5). " \
|
|
98
|
+
"Migrate #{@path} with: gemvault upgrade #{@path}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Gemvault
|
|
2
|
+
# Emits deprecation notices once per unique message, to a configurable IO
|
|
3
|
+
# (stderr in production). Silenceable per-block or via an environment opt-out
|
|
4
|
+
# so automated pipelines that consume a legacy vault are not spammed.
|
|
5
|
+
module Deprecation
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
ENV_KEY = "GEMVAULT_SILENCE_DEPRECATIONS".freeze
|
|
9
|
+
|
|
10
|
+
attr_writer :output
|
|
11
|
+
|
|
12
|
+
def output
|
|
13
|
+
@output ||= $stderr
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def warn_once(message)
|
|
17
|
+
return if silenced?
|
|
18
|
+
return unless seen.add?(message)
|
|
19
|
+
|
|
20
|
+
output.puts("gemvault: #{message}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def silence
|
|
24
|
+
previous = @silenced
|
|
25
|
+
@silenced = true
|
|
26
|
+
begin
|
|
27
|
+
yield
|
|
28
|
+
ensure
|
|
29
|
+
@silenced = previous
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def silenced?
|
|
34
|
+
@silenced || ENV.key?(ENV_KEY)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset!
|
|
38
|
+
@seen = Set.new
|
|
39
|
+
@silenced = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def seen
|
|
45
|
+
@seen ||= Set.new
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/gemvault/gem_entry.rb
CHANGED
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
module Gemvault
|
|
2
|
-
# Value object for one gem
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
# Value object for one gem's identity (name, version, platform) and the time
|
|
3
|
+
# it was stored in a vault. Equality, hashing, and to_h come from Data.
|
|
4
|
+
class GemEntry < Data.define(:name, :version, :platform, :created_at)
|
|
5
|
+
RUBY_PLATFORM_NAME = "ruby".freeze
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
##
|
|
8
|
+
# Builds an entry from a <tt>Gem::Specification</tt> (or any object with
|
|
9
|
+
# +name+, +version+, and +platform+), stringifying the version and platform.
|
|
10
|
+
def self.from_spec(spec, created_at: nil)
|
|
11
|
+
new(name: spec.name, version: spec.version.to_s, platform: spec.platform.to_s, created_at:)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
def
|
|
14
|
-
|
|
14
|
+
def initialize(name:, version:, platform: RUBY_PLATFORM_NAME, created_at: nil)
|
|
15
|
+
super
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def
|
|
18
|
-
"#{full_name}.gem"
|
|
19
|
-
end
|
|
18
|
+
def ruby_platform? = platform == RUBY_PLATFORM_NAME
|
|
20
19
|
|
|
21
|
-
def
|
|
22
|
-
|
|
20
|
+
def full_name
|
|
21
|
+
ruby_platform? ? "#{name}-#{version}" : "#{name}-#{version}-#{platform}"
|
|
23
22
|
end
|
|
24
23
|
|
|
25
|
-
def
|
|
26
|
-
other.is_a?(self.class) &&
|
|
27
|
-
name == other.name &&
|
|
28
|
-
version == other.version &&
|
|
29
|
-
platform == other.platform
|
|
30
|
-
end
|
|
24
|
+
def filename = "#{full_name}.gem"
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
def same_identity_as?(other)
|
|
27
|
+
name == other.name && version == other.version && platform == other.platform
|
|
28
|
+
end
|
|
33
29
|
|
|
34
|
-
def
|
|
35
|
-
|
|
30
|
+
def to_s
|
|
31
|
+
ruby_platform? ? "#{name}-#{version}" : "#{name}-#{version} (#{platform})"
|
|
36
32
|
end
|
|
37
33
|
end
|
|
38
34
|
end
|