yobi 0.1.0 → 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.
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
3
  require "time"
5
- require "delegate"
6
4
 
7
5
  module Yobi
8
6
  class Repository
@@ -32,33 +30,67 @@ module Yobi
32
30
  a.repeat_flag(:tag, tags)
33
31
  a.end_of_options.append(dirs)
34
32
  end
35
- execution = run_restic(argv)
36
- LsOutcome.new(execution[:exit_code], execution[:output])
33
+ output = Yobi::ResticOutput.new(transform: Yobi::LsMessageWrapper)
34
+ execution = run_restic(argv, output: output)
35
+ LsOutcome.new(execution)
36
+ end
37
+ end
38
+
39
+ # Classifies a raw ls message Hash by its own message_type, wrapping it
40
+ # in the matching typed class. Used both as the {Yobi::ResticOutput}
41
+ # `transform:` for {Yobi::Repository#ls} and by {Yobi::LsOutcome}'s own
42
+ # post-hoc accessors.
43
+ #
44
+ # @private
45
+ module LsMessageWrapper
46
+ # @param raw [Hash]
47
+ # @return [Yobi::Snapshot, Yobi::Node, Hash]
48
+ # the raw Hash itself for a message_type this version of Yobi doesn't recognize
49
+ def self.call(raw)
50
+ case raw["message_type"]
51
+ when "snapshot" then Snapshot.new(raw)
52
+ when "node" then Node.new(raw)
53
+ else raw
54
+ end
37
55
  end
38
56
  end
39
57
 
40
58
  # The outcome of one {Yobi::Repository#ls} call.
41
- class LsOutcome < Struct.new(:exit_code, :output)
59
+ class LsOutcome
60
+ # @return [Yobi::ResticOutput]
61
+ attr_reader :output
62
+
63
+ # @private
64
+ def initialize(execution)
65
+ @output = execution[:output]
66
+ end
67
+
42
68
  # @return [Yobi::Snapshot] the resolved snapshot, useful when `snapshot_id:` was `"latest"`
43
69
  def snapshot
44
- @snapshot ||= Yobi::Snapshot.new(JSON.parse(snapshot_line))
70
+ @snapshot ||= output.messages("snapshot").first
45
71
  end
46
72
 
47
- # @return [Enumerable<Yobi::LsEntry>]
48
- def entries
49
- @entries ||= LsEntries.new(output)
73
+ # @return [Enumerable<Yobi::Node>]
74
+ def nodes
75
+ @nodes ||= output.messages("node")
50
76
  end
77
+ alias_method :entries, :nodes
51
78
 
52
- private
53
-
54
- def snapshot_line
55
- offset = output.index["snapshot"].first
56
- output.read_line_at(offset)
79
+ # @return [void]
80
+ def pretty_print(q)
81
+ q.object_group(self) do
82
+ q.breakable
83
+ q.pp snapshot
84
+ q.breakable
85
+ q.pp nodes
86
+ end
57
87
  end
58
88
  end
59
89
 
60
- # One file/directory entry from a {Yobi::Repository#ls} call.
61
- class LsEntry < SimpleDelegator
90
+ # One file/directory entry from a {Yobi::Repository#ls} call. Restic's
91
+ # own term for this is a "node" - the same structure it uses internally
92
+ # for every entry in a snapshot's tree, not just ones `ls` happens to show.
93
+ class Node < Yobi::FancyHash
62
94
  # @return [String]
63
95
  def name
64
96
  self["name"]
@@ -114,25 +146,4 @@ module Yobi
114
146
  @ctime ||= Time.parse(self["ctime"])
115
147
  end
116
148
  end
117
-
118
- # Enumerable over every {Yobi::LsEntry} in a run.
119
- class LsEntries
120
- include Enumerable
121
-
122
- # @param output [Yobi::ResticOutput]
123
- def initialize(output)
124
- @output = output
125
- end
126
-
127
- # @yieldparam entry [Yobi::LsEntry]
128
- # @return [Enumerator] if no block is given
129
- def each
130
- return enum_for(:each) unless block_given?
131
-
132
- @output.index["node"].each do |offset|
133
- line = @output.read_line_at(offset)
134
- yield LsEntry.new(JSON.parse(line))
135
- end
136
- end
137
- end
138
149
  end
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
- require "tempfile"
5
4
 
6
5
  module Yobi
7
6
  class Repository
8
7
  # `restic mount`: serves this repository as a read-only FUSE
9
8
  # filesystem at `mountpoint`, which must already exist.
10
9
  #
11
- # Without a block, returns a {Yobi::Mount} once Restic reports itself
12
- # ready; call `#stop` yourself once done. With one, yields the `Mount`
13
- # and stops it automatically once the block returns or raises.
10
+ # Without a block, returns a {Yobi::MountHandle} once Restic reports
11
+ # itself ready; call `#stop` yourself once done. With one, yields the
12
+ # `MountHandle` and stops it automatically once the block returns or raises.
14
13
  #
15
14
  # @param mountpoint [String] must already exist
16
15
  # @param hosts [Array<String>, String] restrict which snapshots appear under `snapshots/`
@@ -22,8 +21,8 @@ module Yobi
22
21
  # @param path_templates [Array<String>, String] directory naming scheme(s) under `snapshots/`
23
22
  # @param time_template [String, nil] directory naming scheme for time-based paths
24
23
  # @param ready_timeout [Numeric] seconds to wait for Restic's readiness message before raising
25
- # @yieldparam mount [Yobi::Mount]
26
- # @return [Yobi::Mount] if no block is given
24
+ # @yieldparam mount [Yobi::MountHandle]
25
+ # @return [Yobi::MountHandle] if no block is given
27
26
  # @return [Object] the block's own return value, otherwise
28
27
  # @raise [Yobi::MountTimeout]
29
28
  def mount(mountpoint:, hosts: [], paths: [], tags: [], allow_other: false,
@@ -59,7 +58,7 @@ module Yobi
59
58
  end
60
59
 
61
60
  class Restic
62
- # Restic's own stdout line signaling a mount is ready to serve.
61
+ # @private
63
62
  READY_LINE = "Now serving the repository at"
64
63
 
65
64
  # For {Yobi::Repository#mount}. Spawns Restic and waits for its
@@ -70,43 +69,42 @@ module Yobi
70
69
  # @param mountpoint [String]
71
70
  # @param extra_env [Hash{String => String}]
72
71
  # @param ready_timeout [Numeric]
73
- # @return [Yobi::Mount]
72
+ # @return [Yobi::MountHandle]
74
73
  # @raise [Yobi::MountTimeout]
75
74
  def run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10)
76
75
  ensure_minimum_version!
77
- file = Tempfile.new("yobi-restic-output")
78
- file.unlink
76
+ output = Yobi::ResticOutput.new
79
77
 
80
- stdin, output, wait_thr = Open3.popen2e(env.merge(extra_env), restic_path, *argv)
78
+ stdin, pipe, wait_thr = Open3.popen2e(env.merge(extra_env), restic_path, *argv)
81
79
  stdin.close
82
80
 
83
- case wait_for_ready(output, file, mountpoint, ready_timeout)
81
+ case wait_for_ready(pipe, output.file, mountpoint, ready_timeout)
84
82
  when :ready
85
- Yobi::Mount.new(wait_thr: wait_thr, mountpoint: mountpoint, output: output, output_file: file, argv: argv)
83
+ Yobi::MountHandle.new(wait_thr: wait_thr, mountpoint: mountpoint, pipe: pipe, output: output, argv: argv)
86
84
  when :timeout
87
85
  Process.kill("INT", wait_thr.pid)
88
86
  wait_thr.value
89
87
  raise Yobi::MountTimeout.new(argv: argv, timeout: ready_timeout)
90
88
  when :exited
91
89
  status = wait_thr.value
92
- self.class.dispatch(exit_code: status.exitstatus, output: Yobi::ResticOutput.new(file), argv: argv)
90
+ self.class.dispatch(exit_code: status.exitstatus, output: output, argv: argv)
93
91
  end
94
92
  rescue Errno::ENOENT
95
- file&.close
93
+ output&.file&.close
96
94
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
97
95
  end
98
96
 
99
97
  private
100
98
 
101
- def wait_for_ready(output, file, mountpoint, timeout)
99
+ def wait_for_ready(pipe, file, mountpoint, timeout)
102
100
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
103
101
 
104
102
  loop do
105
103
  remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
106
104
  return :timeout if remaining <= 0
107
- return :timeout if IO.select([output], nil, nil, remaining).nil?
105
+ return :timeout if IO.select([pipe], nil, nil, remaining).nil?
108
106
 
109
- line = output.gets
107
+ line = pipe.gets
110
108
  return :exited if line.nil?
111
109
 
112
110
  file.write(line)
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "delegate"
5
-
6
3
  module Yobi
7
4
  class Repository
8
5
  # `restic restore`: extracts a snapshot's contents to a target directory.
@@ -58,60 +55,65 @@ module Yobi
58
55
  a.short_flag(:vv) if verbose
59
56
  a.flag(:verify) if verify
60
57
  end
61
- execution = if block
62
- run_restic(argv) { |raw| dispatch_restore_message(raw, &block) }
63
- else
64
- run_restic(argv)
65
- end
66
- RestoreOutcome.new(execution[:exit_code], execution[:output])
67
- end
68
58
 
69
- private
59
+ output = Yobi::ResticOutput.new(transform: Yobi::RestoreMessageWrapper)
60
+ # Restic's own restore summary isn't meant for the live block - only
61
+ # {RestoreOutcome#summary}'s post-hoc access sees it - but it shares
62
+ # the same @transform (needed there since it isn't a status/verbose_status
63
+ # message), so it has to be filtered back out here instead.
64
+ live_block = block && proc { |message| block.call(message) if message.is_a?(RestoreStatus) || message.is_a?(RestoreVerboseStatus) }
65
+ execution = run_restic(argv, output: output, &live_block)
66
+ RestoreOutcome.new(execution)
67
+ end
68
+ end
70
69
 
71
- def dispatch_restore_message(raw)
70
+ # Classifies a raw restore message Hash by its own message_type, wrapping
71
+ # it in the matching typed class. Used both by {Yobi::Repository#restore}
72
+ # (as the {Yobi::ResticOutput} `transform:` for a live streaming run) and
73
+ # by {Yobi::RestoreOutcome}'s own post-hoc accessors.
74
+ #
75
+ # @private
76
+ module RestoreMessageWrapper
77
+ # @param raw [Hash]
78
+ # @return [Yobi::RestoreStatus, Yobi::RestoreVerboseStatus, Hash]
79
+ # the raw Hash itself for a message_type this version of Yobi doesn't recognize
80
+ def self.call(raw)
72
81
  case raw["message_type"]
73
- when "status"
74
- yield Yobi::RestoreStatus.new(raw)
75
- when "verbose_status"
76
- yield Yobi::RestoreVerboseStatus.new(raw)
82
+ when "status" then RestoreStatus.new(raw)
83
+ when "verbose_status" then RestoreVerboseStatus.new(raw)
84
+ else raw
77
85
  end
78
86
  end
79
87
  end
80
88
 
81
89
  # The outcome of one {Yobi::Repository#restore} call.
82
- class RestoreOutcome < Struct.new(:exit_code, :output)
83
- # @return [Hash] Restic's own `"summary"` fields (`"total_files"`, `"files_restored"`, ...)
84
- def report
85
- @report ||= summary_hash
86
- end
87
-
88
- private
90
+ class RestoreOutcome
91
+ # @return [Yobi::ResticOutput]
92
+ attr_reader :output
89
93
 
90
- def summary_hash
91
- line = output.last_line
92
- hash = line && JSON.parse(line)
93
- if hash && hash["message_type"] == "summary"
94
- hash
95
- else
96
- find_summary || {}
97
- end
94
+ # @private
95
+ def initialize(execution)
96
+ @output = execution[:output]
98
97
  end
99
98
 
100
- def find_summary
101
- output.each_line do |line|
102
- next if line.strip.empty?
99
+ # @return [Hash] Restic's own `"summary"` fields (`"total_files"`, `"files_restored"`, ...)
100
+ def summary
101
+ @summary ||= output.messages("summary").first || {}
102
+ end
103
+ alias_method :report, :summary
103
104
 
104
- hash = JSON.parse(line)
105
- return hash if hash["message_type"] == "summary"
105
+ # @return [void]
106
+ def pretty_print(q)
107
+ q.object_group(self) do
108
+ q.breakable
109
+ q.pp summary
106
110
  end
107
-
108
- nil
109
111
  end
110
112
  end
111
113
 
112
114
  # One `"status"` message from a live restore run.
113
115
  # https://restic.readthedocs.io/en/stable/075_scripting.html#restore
114
- class RestoreStatus < SimpleDelegator
116
+ class RestoreStatus < Yobi::FancyHash
115
117
  # @return [Float, nil]
116
118
  def percent_done
117
119
  self["percent_done"]
@@ -140,7 +142,7 @@ module Yobi
140
142
 
141
143
  # One `"verbose_status"` message from a restore run, one per file. Only
142
144
  # emitted when `verbose: true` is passed to {Yobi::Repository#restore}.
143
- class RestoreVerboseStatus < SimpleDelegator
145
+ class RestoreVerboseStatus < Yobi::FancyHash
144
146
  # @return [String]
145
147
  def action
146
148
  self["action"]
@@ -12,7 +12,7 @@ module Yobi
12
12
  # @param compact [Boolean] compact the printed listing
13
13
  # @param group_by [String, nil] group results, e.g. `"host"`
14
14
  # @param latest [Integer, nil] limit to the N most recent per group
15
- # @return [Enumerable<Yobi::Snapshot>]
15
+ # @return [Array<Yobi::Snapshot>]
16
16
  def snapshots(tags: [], hosts: [], paths: [], compact: false, group_by: nil, latest: nil)
17
17
  argv = build_argv("snapshots") do |a|
18
18
  a.repeat_flag(:tag, tags)
@@ -23,34 +23,7 @@ module Yobi
23
23
  a.flag(:latest, latest) unless latest.nil?
24
24
  end
25
25
  execution = run_restic(argv)
26
- Snapshots.new(execution[:output])
27
- end
28
- end
29
-
30
- # Enumerable over every {Yobi::Snapshot} in one {Yobi::Repository#snapshots} call.
31
- class Snapshots
32
- include Enumerable
33
-
34
- # @return [Yobi::ResticOutput]
35
- attr_reader :output
36
-
37
- # @param output [Yobi::ResticOutput]
38
- def initialize(output)
39
- @output = output
40
- end
41
-
42
- # @yieldparam snapshot [Yobi::Snapshot]
43
- # @return [Enumerator] if no block is given
44
- def each(&block)
45
- return enum_for(:each) unless block_given?
46
-
47
- entries.each(&block)
48
- end
49
-
50
- private
51
-
52
- def entries
53
- @entries ||= JSON.parse(output.to_s).map { |raw| Yobi::Snapshot.new(raw) }
26
+ JSON.parse(execution[:output].to_s).map { |raw| Snapshot.new(raw) }
54
27
  end
55
28
  end
56
29
  end
@@ -4,8 +4,7 @@ require "json"
4
4
 
5
5
  module Yobi
6
6
  class Repository
7
- # Maps every valid `mode:` value for {#stats}, as both String and Symbol,
8
- # to the String Restic itself expects.
7
+ # @private
9
8
  STATS_MODES = %w[restore-size files-by-contents blobs-per-file raw-data].each_with_object({}) do |value, hash|
10
9
  hash[value] = value
11
10
  hash[value.tr("-", "_").to_sym] = value
@@ -18,7 +17,7 @@ module Yobi
18
17
  # @param mode [String, Symbol, nil] `"restore-size"`/`:restore_size` (default), `"files-by-contents"`/`:files_by_contents`, `"blobs-per-file"`/`:blobs_per_file`, or `"raw-data"`/`:raw_data`
19
18
  # @param paths [Array<String>, String] filter by originally backed-up path(s)
20
19
  # @param tags [Array<String>, String] filter by tag(s)
21
- # @return [Hash] `"total_size"`, `"total_file_count"`, `"snapshots_count"`, ...
20
+ # @return [Yobi::RepositoryStats]
22
21
  # @raise [ArgumentError] if `mode:` isn't one of the values listed above
23
22
  def stats(snapshot_ids: [], hosts: [], mode: nil, paths: [], tags: [])
24
23
  argv = build_argv("stats", snapshot_ids) do |a|
@@ -30,7 +29,51 @@ module Yobi
30
29
  a.repeat_flag(:tag, tags)
31
30
  end
32
31
  execution = run_restic(argv)
33
- JSON.parse(execution[:output].to_s)
32
+ RepositoryStats.new(JSON.parse(execution[:output].to_s))
33
+ end
34
+ end
35
+
36
+ # The result of one {Yobi::Repository#stats} call.
37
+ # https://restic.readthedocs.io/en/stable/075_scripting.html#stats
38
+ class RepositoryStats < Yobi::FancyHash
39
+ # @return [Integer] repository size in bytes
40
+ def total_size
41
+ self["total_size"] || 0
42
+ end
43
+
44
+ # @return [Integer] number of files backed up in the repository
45
+ def total_file_count
46
+ self["total_file_count"] || 0
47
+ end
48
+
49
+ # @return [Integer] number of blobs in the repository
50
+ def total_blob_count
51
+ self["total_blob_count"] || 0
52
+ end
53
+
54
+ # @return [Integer] number of processed snapshots
55
+ def snapshots_count
56
+ self["snapshots_count"] || 0
57
+ end
58
+
59
+ # @return [Integer] repository size in bytes if blobs were uncompressed
60
+ def total_uncompressed_size
61
+ self["total_uncompressed_size"] || 0
62
+ end
63
+
64
+ # @return [Float] factor by which the already compressed data has shrunk due to compression
65
+ def compression_ratio
66
+ self["compression_ratio"] || 0.0
67
+ end
68
+
69
+ # @return [Float] percentage of already compressed data
70
+ def compression_progress
71
+ self["compression_progress"] || 0.0
72
+ end
73
+
74
+ # @return [Float] overall space saving due to compression
75
+ def compression_space_saving
76
+ self["compression_space_saving"] || 0.0
34
77
  end
35
78
  end
36
79
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "delegate"
5
-
6
3
  module Yobi
7
4
  class Repository
8
5
  # `restic tag`: modifies tags on existing snapshots. Tags are part of
@@ -26,56 +23,65 @@ module Yobi
26
23
  a.repeat_flag(:host, hosts)
27
24
  a.repeat_flag(:path, paths)
28
25
  end
29
- execution = run_restic(argv)
30
- TagOutcome.new(execution[:exit_code], execution[:output])
26
+ output = Yobi::ResticOutput.new(transform: Yobi::TagMessageWrapper)
27
+ execution = run_restic(argv, output: output)
28
+ TagOutcome.new(execution)
31
29
  end
32
30
  end
33
31
 
34
- # The outcome of one {Yobi::Repository#tag} call.
35
- class TagOutcome < Struct.new(:exit_code, :output)
36
- # @return [Hash] Restic's own `"summary"` fields (`"changed_snapshots"`)
37
- def report
38
- @report ||= summary_hash
32
+ # Classifies a raw tag message Hash by its own message_type, wrapping it
33
+ # in the matching typed class. Used both as the {Yobi::ResticOutput}
34
+ # `transform:` for {Yobi::Repository#tag} and by {Yobi::TagOutcome}'s own
35
+ # post-hoc accessors.
36
+ #
37
+ # @private
38
+ module TagMessageWrapper
39
+ # @param raw [Hash]
40
+ # @return [Yobi::TagChange, Yobi::TagSummary, Hash]
41
+ # the raw Hash itself for a message_type this version of Yobi doesn't recognize
42
+ def self.call(raw)
43
+ case raw["message_type"]
44
+ when "changed" then TagChange.new(raw)
45
+ when "summary" then TagSummary.new(raw)
46
+ else raw
47
+ end
39
48
  end
49
+ end
40
50
 
41
- # @return [Array<Yobi::TagChange>] one per snapshot actually modified
42
- def changes
43
- @changes ||= output.each_line.filter_map do |line|
44
- next if line.strip.empty?
51
+ # The outcome of one {Yobi::Repository#tag} call.
52
+ class TagOutcome
53
+ # @return [Yobi::ResticOutput]
54
+ attr_reader :output
45
55
 
46
- hash = JSON.parse(line)
47
- Yobi::TagChange.new(hash) if hash["message_type"] == "changed"
48
- end
56
+ # @private
57
+ def initialize(execution)
58
+ @output = execution[:output]
49
59
  end
50
60
 
51
- private
52
-
53
- def summary_hash
54
- line = output.last_line
55
- hash = line && JSON.parse(line)
56
- if hash && hash["message_type"] == "summary"
57
- hash
58
- else
59
- find_summary || {}
60
- end
61
+ # @return [Yobi::TagSummary] Restic's own `"summary"` fields (`"changed_snapshots"`)
62
+ def summary
63
+ @summary ||= output.messages("summary").first || TagSummary.new({})
61
64
  end
65
+ alias_method :report, :summary
62
66
 
63
- def find_summary
64
- output.each_line do |line|
65
- next if line.strip.empty?
67
+ # @return [Enumerable<Yobi::TagChange>] one per snapshot actually modified
68
+ def changes
69
+ @changes ||= output.messages("changed")
70
+ end
66
71
 
67
- hash = JSON.parse(line)
68
- return hash if hash["message_type"] == "summary"
72
+ # @return [void]
73
+ def pretty_print(q)
74
+ q.object_group(self) do
75
+ q.breakable
76
+ q.pp summary
69
77
  end
70
-
71
- nil
72
78
  end
73
79
  end
74
80
 
75
81
  # One `"changed"` message from a tag run. `old_snapshot_id` is now stale:
76
82
  # tags are part of a snapshot's content-addressed identity, so changing
77
83
  # them produces a new snapshot ID.
78
- class TagChange < SimpleDelegator
84
+ class TagChange < Yobi::FancyHash
79
85
  # @return [String]
80
86
  def old_snapshot_id
81
87
  self["old_snapshot_id"]
@@ -86,4 +92,13 @@ module Yobi
86
92
  self["new_snapshot_id"]
87
93
  end
88
94
  end
95
+
96
+ # The `"summary"` message from a tag run, the final result once the
97
+ # command finishes. Dispatched to {Yobi::TagOutcome#summary}.
98
+ class TagSummary < Yobi::FancyHash
99
+ # @return [Integer]
100
+ def changed_snapshots
101
+ self["changed_snapshots"] || 0
102
+ end
103
+ end
89
104
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "json"
5
4
 
6
5
  module Yobi
7
6
  # One Restic repository. Every Restic subcommand that operates on a
@@ -9,13 +8,13 @@ module Yobi
9
8
  class Repository
10
9
  # @return [String] the repository location
11
10
  attr_reader :url
12
- # @return [String, Array, Symbol, #call, nil] the repository's encryption password, as given to {#initialize}
11
+ # @return [String, Array, Symbol, #call] the repository's encryption password, as given to {#initialize}
13
12
  attr_reader :password
14
13
  # @return [Hash, #call] the storage backend's own credentials, as given to {#initialize}
15
14
  attr_reader :backend_credentials
16
15
 
17
16
  # @param url [String] the repository location, e.g. `"s3:s3.amazonaws.com/bucket"`
18
- # @param password [String, Array, Symbol, #call, nil] a literal password; a
17
+ # @param password [String, Array, Symbol, #call] a literal password; a
19
18
  # `[:command, "..."]`/`[:file, "..."]` tuple, resolved natively by Restic
20
19
  # itself; `:insecure_no_password`; or anything responding to `#call`
21
20
  # (invoked fresh immediately before every Restic invocation)
@@ -24,7 +23,10 @@ module Yobi
24
23
  # responding to `#call` returning such a Hash
25
24
  # @param restic [Yobi::Restic, String, nil] a `Restic` instance to share,
26
25
  # a bare Restic binary path, or `nil` to create a default one
27
- def initialize(url:, password: nil, backend_credentials: {}, restic: nil)
26
+ # @raise [ArgumentError] if `password:` is `nil`
27
+ def initialize(url:, password:, backend_credentials: {}, restic: nil)
28
+ raise ArgumentError, "password: is required (use :insecure_no_password if the repository truly has none)" if password.nil?
29
+
28
30
  @url, @extracted_rest_credentials = extract_rest_credentials(url)
29
31
  @password = password
30
32
  @backend_credentials = backend_credentials
@@ -43,47 +45,10 @@ module Yobi
43
45
  "#<#{self.class} url=#{url.inspect} password=#{redacted_password.inspect} backend_credentials=#{redacted_backend_credentials.inspect}>"
44
46
  end
45
47
 
46
- # `restic init`: creates the repository at {#url}.
47
- #
48
- # @param copy_chunker_params [Boolean] copy chunker parameters from `from_repo:`/`from_repository_file:`
49
- # @param from_insecure_no_password [Boolean]
50
- # @param from_key_hint [String, nil]
51
- # @param from_password_command [String, nil]
52
- # @param from_password_file [String, nil]
53
- # @param from_repo [String, nil]
54
- # @param from_repository_file [String, nil]
55
- # @param repository_version [String, nil]
56
- # @return [Hash] Restic's own `"initialized"` message
57
- def init(copy_chunker_params: false, from_insecure_no_password: false, from_key_hint: nil,
58
- from_password_command: nil, from_password_file: nil, from_repo: nil, from_repository_file: nil,
59
- repository_version: nil)
60
- argv = build_argv("init") do |a|
61
- a.flag(:copy_chunker_params) if copy_chunker_params
62
- a.flag(:from_insecure_no_password) if from_insecure_no_password
63
- a.flag(:from_key_hint, from_key_hint) unless from_key_hint.nil?
64
- a.flag(:from_password_command, from_password_command) unless from_password_command.nil?
65
- a.flag(:from_password_file, from_password_file) unless from_password_file.nil?
66
- a.flag(:from_repo, from_repo) unless from_repo.nil?
67
- a.flag(:from_repository_file, from_repository_file) unless from_repository_file.nil?
68
- a.flag(:repository_version, repository_version) unless repository_version.nil?
69
- end
70
- execution = run_restic(argv)
71
- JSON.parse(execution[:output].to_s)
72
- end
73
-
74
- # `restic cat config`: the repository's own config document.
75
- #
76
- # @return [Hash]
77
- # @raise [Yobi::RepositoryNotFound, Yobi::AuthenticationFailed]
78
- def config
79
- execution = run_restic(build_argv("cat", "config"))
80
- JSON.parse(execution[:output].to_s)
81
- end
82
-
83
48
  private
84
49
 
85
- def run_restic(argv, extra_env: {}, &block)
86
- @restic.run(argv, extra_env: env.merge(extra_env), &block)
50
+ def run_restic(argv, extra_env: {}, output: nil, &block)
51
+ @restic.run(argv, extra_env: env.merge(extra_env), output: output, &block)
87
52
  end
88
53
 
89
54
  def build_argv(*base)