yobi 0.1.0 → 0.3.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/CHANGELOG.md +24 -0
- data/README.md +93 -124
- data/lib/yobi/argv_builder.rb +5 -0
- data/lib/yobi/errors.rb +6 -10
- data/lib/yobi/fancy_hash.rb +31 -0
- data/lib/yobi/io_handle.rb +9 -7
- data/lib/yobi/{mount.rb → mount_handle.rb} +11 -10
- data/lib/yobi/repository/backup.rb +79 -85
- data/lib/yobi/repository/cat.rb +10 -0
- data/lib/yobi/repository/check.rb +61 -45
- data/lib/yobi/repository/copy.rb +9 -11
- data/lib/yobi/repository/diff.rb +130 -47
- data/lib/yobi/repository/dump.rb +1 -1
- data/lib/yobi/repository/find.rb +11 -33
- data/lib/yobi/repository/forget.rb +6 -48
- data/lib/yobi/repository/init.rb +71 -0
- data/lib/yobi/repository/key.rb +61 -52
- data/lib/yobi/repository/ls.rb +48 -37
- data/lib/yobi/repository/mount.rb +16 -18
- data/lib/yobi/repository/restore.rb +42 -40
- data/lib/yobi/repository/snapshots.rb +2 -29
- data/lib/yobi/repository/stats.rb +47 -4
- data/lib/yobi/repository/tag.rb +50 -35
- data/lib/yobi/repository.rb +56 -43
- data/lib/yobi/restic.rb +73 -32
- data/lib/yobi/restic_output.rb +169 -12
- data/lib/yobi/snapshot.rb +81 -2
- data/lib/yobi/version.rb +2 -2
- data/lib/yobi.rb +2 -1
- data/sig/yobi.rbs +254 -140
- metadata +6 -8
- data/Rakefile +0 -12
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
# Base class for every wrapper around one raw Restic JSON object.
|
|
5
|
+
class FancyHash < Hash
|
|
6
|
+
# @private
|
|
7
|
+
def initialize(raw)
|
|
8
|
+
super()
|
|
9
|
+
replace(raw)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [String]
|
|
13
|
+
def inspect
|
|
14
|
+
"#<#{self.class} #{super}>"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [void]
|
|
18
|
+
def pretty_print(q)
|
|
19
|
+
q.object_group(self) do
|
|
20
|
+
each do |key, value|
|
|
21
|
+
next if key == "message_type"
|
|
22
|
+
|
|
23
|
+
value = public_send(key) if self.class.method_defined?(key, false)
|
|
24
|
+
q.breakable
|
|
25
|
+
q.text "#{key}="
|
|
26
|
+
q.pp(value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/yobi/io_handle.rb
CHANGED
|
@@ -22,14 +22,11 @@ module Yobi
|
|
|
22
22
|
# @return [Integer] the Restic process's pid
|
|
23
23
|
attr_reader :pid
|
|
24
24
|
|
|
25
|
-
# @
|
|
26
|
-
|
|
27
|
-
# @param output_file [File]
|
|
28
|
-
# @param argv [Array<String>]
|
|
29
|
-
def initialize(io, pid:, output_file:, argv:)
|
|
25
|
+
# @private
|
|
26
|
+
def initialize(io, pid:, output:, argv:)
|
|
30
27
|
@io = io
|
|
31
28
|
@pid = pid
|
|
32
|
-
@
|
|
29
|
+
@output = output
|
|
33
30
|
@argv = argv
|
|
34
31
|
@closed = false
|
|
35
32
|
end
|
|
@@ -39,6 +36,11 @@ module Yobi
|
|
|
39
36
|
@closed
|
|
40
37
|
end
|
|
41
38
|
|
|
39
|
+
# @return [String]
|
|
40
|
+
def inspect
|
|
41
|
+
"#<#{self.class} pid=#{pid} closed=#{closed?}>"
|
|
42
|
+
end
|
|
43
|
+
|
|
42
44
|
# Reaps the Restic process and raises based on its exit code. Safe to
|
|
43
45
|
# call more than once.
|
|
44
46
|
#
|
|
@@ -49,7 +51,7 @@ module Yobi
|
|
|
49
51
|
@closed = true
|
|
50
52
|
@io.close unless @io.closed?
|
|
51
53
|
_, status = Process.wait2(@pid)
|
|
52
|
-
Restic.dispatch(exit_code: status.exitstatus, output:
|
|
54
|
+
Restic.dispatch(exit_code: status.exitstatus, output: @output, argv: @argv)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
# Yields binary-safe chunks of {#io} until EOF, then calls {#close}.
|
|
@@ -5,20 +5,16 @@ module Yobi
|
|
|
5
5
|
# Restic has reported itself ready. The mounted filesystem itself is
|
|
6
6
|
# browsed with ordinary file I/O at {#mountpoint}; this object only
|
|
7
7
|
# manages the Restic process's lifetime.
|
|
8
|
-
class
|
|
8
|
+
class MountHandle
|
|
9
9
|
# @return [String] the path the repository is mounted at
|
|
10
10
|
attr_reader :mountpoint
|
|
11
11
|
|
|
12
|
-
# @
|
|
13
|
-
|
|
14
|
-
# @param output [IO]
|
|
15
|
-
# @param output_file [File]
|
|
16
|
-
# @param argv [Array<String>]
|
|
17
|
-
def initialize(wait_thr:, mountpoint:, output:, output_file:, argv:)
|
|
12
|
+
# @private
|
|
13
|
+
def initialize(wait_thr:, mountpoint:, pipe:, output:, argv:)
|
|
18
14
|
@wait_thr = wait_thr
|
|
19
15
|
@mountpoint = mountpoint
|
|
16
|
+
@pipe = pipe
|
|
20
17
|
@output = output
|
|
21
|
-
@output_file = output_file
|
|
22
18
|
@argv = argv
|
|
23
19
|
@stopped = false
|
|
24
20
|
end
|
|
@@ -33,6 +29,11 @@ module Yobi
|
|
|
33
29
|
@stopped
|
|
34
30
|
end
|
|
35
31
|
|
|
32
|
+
# @return [String]
|
|
33
|
+
def inspect
|
|
34
|
+
"#<#{self.class} pid=#{pid} mountpoint=#{mountpoint.inspect} stopped=#{stopped?}>"
|
|
35
|
+
end
|
|
36
|
+
|
|
36
37
|
# Sends the Restic process SIGINT, waits for it to unmount and exit,
|
|
37
38
|
# then raises based on its exit code. Safe to call more than once, and
|
|
38
39
|
# safe to call after the mount has already been stopped externally
|
|
@@ -52,13 +53,13 @@ module Yobi
|
|
|
52
53
|
|
|
53
54
|
drain_remaining_output
|
|
54
55
|
status = @wait_thr.value
|
|
55
|
-
Restic.dispatch(exit_code: status.exitstatus, output:
|
|
56
|
+
Restic.dispatch(exit_code: status.exitstatus, output: @output, argv: @argv)
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
private
|
|
59
60
|
|
|
60
61
|
def drain_remaining_output
|
|
61
|
-
@
|
|
62
|
+
@pipe.each_line { |line| @output.file.write(line) }
|
|
62
63
|
rescue IOError, Errno::EBADF
|
|
63
64
|
nil
|
|
64
65
|
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
3
|
require "time"
|
|
5
|
-
require "delegate"
|
|
6
4
|
require "shellwords"
|
|
7
5
|
|
|
8
6
|
module Yobi
|
|
@@ -40,7 +38,7 @@ module Yobi
|
|
|
40
38
|
# @param time [String, nil] timestamp to record instead of now
|
|
41
39
|
# @param verbose [Boolean] stream a {Yobi::BackupVerboseStatus} per file to the block
|
|
42
40
|
# @param with_atime [Boolean] also store files' access times
|
|
43
|
-
# @yieldparam message [Yobi::BackupStatus, Yobi::BackupError, Yobi::BackupVerboseStatus]
|
|
41
|
+
# @yieldparam message [Yobi::BackupStatus, Yobi::BackupError, Yobi::BackupVerboseStatus, Yobi::BackupSummary]
|
|
44
42
|
# @return [Yobi::BackupOutcome]
|
|
45
43
|
def backup(source:, excludes: [], exclude_files: [], exclude_if_present: [], exclude_larger_than: nil,
|
|
46
44
|
files_from: [], files_from_raw: [], files_from_verbatim: [], iexcludes: [], iexclude_files: [],
|
|
@@ -85,109 +83,126 @@ module Yobi
|
|
|
85
83
|
a.flag(:stdin_from_command)
|
|
86
84
|
a.flag(:stdin_filename, filename)
|
|
87
85
|
a.end_of_options.append(tokenize(command))
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, "invalid source shape"
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
run_restic(argv)
|
|
95
|
-
end
|
|
96
|
-
BackupOutcome.new(execution[:exit_code], execution[:output])
|
|
91
|
+
output = Yobi::ResticOutput.new(transform: Yobi::BackupMessageWrapper)
|
|
92
|
+
execution = run_restic(argv, output: output, &block)
|
|
93
|
+
BackupOutcome.new(execution)
|
|
97
94
|
end
|
|
98
95
|
|
|
99
96
|
private
|
|
100
97
|
|
|
101
|
-
def dispatch_backup_message(raw)
|
|
102
|
-
case raw["message_type"]
|
|
103
|
-
when "status"
|
|
104
|
-
yield Yobi::BackupStatus.new(raw)
|
|
105
|
-
when "error"
|
|
106
|
-
yield Yobi::BackupError.new(raw)
|
|
107
|
-
when "verbose_status"
|
|
108
|
-
yield Yobi::BackupVerboseStatus.new(raw)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
98
|
def tokenize(command)
|
|
113
99
|
case command
|
|
114
100
|
in String => value
|
|
115
101
|
Shellwords.split(value)
|
|
116
102
|
in Array => values
|
|
117
103
|
values.map(&:to_s)
|
|
104
|
+
else
|
|
105
|
+
raise ArgumentError, "invalid stdin_from_command command shape"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Classifies a raw backup message Hash by its own message_type, wrapping
|
|
111
|
+
# it in the matching typed class. Used both by {Yobi::Repository#backup}
|
|
112
|
+
# (as the {Yobi::ResticOutput} `transform:` for a live streaming run) and
|
|
113
|
+
# by {Yobi::BackupOutcome}'s own post-hoc accessors.
|
|
114
|
+
#
|
|
115
|
+
# @private
|
|
116
|
+
module BackupMessageWrapper
|
|
117
|
+
# @param raw [Hash]
|
|
118
|
+
# @return [Yobi::BackupStatus, Yobi::BackupError, Yobi::BackupVerboseStatus, Yobi::BackupSummary, Hash]
|
|
119
|
+
# the raw Hash itself for a message_type this version of Yobi doesn't recognize
|
|
120
|
+
def self.call(raw)
|
|
121
|
+
case raw["message_type"]
|
|
122
|
+
when "status" then BackupStatus.new(raw)
|
|
123
|
+
when "error" then BackupError.new(raw)
|
|
124
|
+
when "verbose_status" then BackupVerboseStatus.new(raw)
|
|
125
|
+
when "summary" then BackupSummary.new(raw)
|
|
126
|
+
else raw
|
|
118
127
|
end
|
|
119
128
|
end
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
# The outcome of one {Yobi::Repository#backup} call.
|
|
123
|
-
class BackupOutcome
|
|
124
|
-
#
|
|
125
|
-
# `source: [:stdin_from_command, ...]` command's own subprocess.
|
|
132
|
+
class BackupOutcome
|
|
133
|
+
# @private
|
|
126
134
|
COMMAND_OUTPUT_LINE_PATTERN = /\Asubprocess [^:]+: (.*)/
|
|
127
135
|
|
|
128
|
-
# @return [
|
|
129
|
-
|
|
130
|
-
exit_code.zero?
|
|
131
|
-
end
|
|
136
|
+
# @return [Yobi::ResticOutput]
|
|
137
|
+
attr_reader :output
|
|
132
138
|
|
|
133
|
-
# @
|
|
134
|
-
def
|
|
135
|
-
|
|
139
|
+
# @private
|
|
140
|
+
def initialize(execution)
|
|
141
|
+
@output = execution[:output]
|
|
136
142
|
end
|
|
137
143
|
|
|
138
144
|
# @return [Enumerable<Yobi::BackupError>]
|
|
139
145
|
def errors
|
|
140
|
-
@errors ||=
|
|
146
|
+
@errors ||= output.messages("error")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Every message from the run, in file order, each wrapped in its own
|
|
150
|
+
# {Yobi::BackupStatus}/{Yobi::BackupError}/{Yobi::BackupVerboseStatus}/{Yobi::BackupSummary}.
|
|
151
|
+
#
|
|
152
|
+
# @return [Enumerable<Yobi::BackupStatus, Yobi::BackupError, Yobi::BackupVerboseStatus, Yobi::BackupSummary>]
|
|
153
|
+
def messages
|
|
154
|
+
@messages ||= output.messages
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
# The `source: [:stdin_from_command, ...]` subprocess's own stderr, if any, de-prefixed.
|
|
144
158
|
#
|
|
145
|
-
# @
|
|
159
|
+
# @yieldparam line [String]
|
|
160
|
+
# @return [Enumerator] if no block is given
|
|
146
161
|
def command_output
|
|
147
|
-
|
|
162
|
+
return enum_for(:command_output) unless block_given?
|
|
163
|
+
|
|
164
|
+
output.stderr_lines.each do |line|
|
|
148
165
|
match = COMMAND_OUTPUT_LINE_PATTERN.match(line)
|
|
149
|
-
match[1] if match
|
|
166
|
+
yield match[1] if match
|
|
150
167
|
end
|
|
151
168
|
end
|
|
152
169
|
|
|
153
|
-
# @return [
|
|
154
|
-
def
|
|
155
|
-
@
|
|
156
|
-
hash = summary_hash
|
|
157
|
-
hash.merge(
|
|
158
|
-
"backup_start" => (Time.parse(hash["backup_start"]) if hash["backup_start"]),
|
|
159
|
-
"backup_end" => (Time.parse(hash["backup_end"]) if hash["backup_end"])
|
|
160
|
-
)
|
|
161
|
-
end
|
|
170
|
+
# @return [Yobi::BackupSummary] Restic's own `"summary"` fields
|
|
171
|
+
def summary
|
|
172
|
+
@summary ||= output.messages("summary").first || BackupSummary.new({})
|
|
162
173
|
end
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
find_summary || {}
|
|
174
|
+
alias_method :report, :summary
|
|
175
|
+
|
|
176
|
+
# @return [void]
|
|
177
|
+
def pretty_print(q)
|
|
178
|
+
q.object_group(self) do
|
|
179
|
+
q.breakable
|
|
180
|
+
q.text("(has errors)") if errors.any?
|
|
181
|
+
q.breakable
|
|
182
|
+
q.pp summary
|
|
173
183
|
end
|
|
174
184
|
end
|
|
185
|
+
end
|
|
175
186
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
# The `"summary"` message from a backup run, the final result once the
|
|
188
|
+
# command finishes. Dispatched to {Yobi::Repository#backup}'s block, and
|
|
189
|
+
# also what {Yobi::BackupOutcome#summary} returns.
|
|
190
|
+
# https://restic.readthedocs.io/en/stable/075_scripting.html#summary
|
|
191
|
+
class BackupSummary < Yobi::FancyHash
|
|
192
|
+
# @return [Time, nil]
|
|
193
|
+
def backup_start
|
|
194
|
+
@backup_start ||= Time.parse(self["backup_start"]) if self["backup_start"]
|
|
195
|
+
end
|
|
183
196
|
|
|
184
|
-
|
|
197
|
+
# @return [Time, nil]
|
|
198
|
+
def backup_end
|
|
199
|
+
@backup_end ||= Time.parse(self["backup_end"]) if self["backup_end"]
|
|
185
200
|
end
|
|
186
201
|
end
|
|
187
202
|
|
|
188
203
|
# One `"status"` message from a live backup run.
|
|
189
204
|
# https://restic.readthedocs.io/en/stable/075_scripting.html#status
|
|
190
|
-
class BackupStatus <
|
|
205
|
+
class BackupStatus < Yobi::FancyHash
|
|
191
206
|
# @return [Float, nil]
|
|
192
207
|
def percent_done
|
|
193
208
|
self["percent_done"]
|
|
@@ -226,7 +241,7 @@ module Yobi
|
|
|
226
241
|
|
|
227
242
|
# One `"error"` message from a backup run.
|
|
228
243
|
# https://restic.readthedocs.io/en/stable/075_scripting.html#error
|
|
229
|
-
class BackupError <
|
|
244
|
+
class BackupError < Yobi::FancyHash
|
|
230
245
|
# @return [String, nil]
|
|
231
246
|
def message
|
|
232
247
|
dig("error", "message")
|
|
@@ -243,30 +258,9 @@ module Yobi
|
|
|
243
258
|
end
|
|
244
259
|
end
|
|
245
260
|
|
|
246
|
-
# Enumerable over every {Yobi::BackupError} in a run.
|
|
247
|
-
class BackupErrors
|
|
248
|
-
include Enumerable
|
|
249
|
-
|
|
250
|
-
# @param output [Yobi::ResticOutput]
|
|
251
|
-
def initialize(output)
|
|
252
|
-
@output = output
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# @yieldparam error [Yobi::BackupError]
|
|
256
|
-
# @return [Enumerator] if no block is given
|
|
257
|
-
def each
|
|
258
|
-
return enum_for(:each) unless block_given?
|
|
259
|
-
|
|
260
|
-
@output.index["error"].each do |offset|
|
|
261
|
-
line = @output.read_line_at(offset)
|
|
262
|
-
yield BackupError.new(JSON.parse(line))
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
|
|
267
261
|
# One `"verbose_status"` message from a backup run, one per file. Only
|
|
268
262
|
# emitted when `verbose: true` is passed to {Yobi::Repository#backup}.
|
|
269
|
-
class BackupVerboseStatus <
|
|
263
|
+
class BackupVerboseStatus < Yobi::FancyHash
|
|
270
264
|
# @return [String]
|
|
271
265
|
def action
|
|
272
266
|
self["action"]
|
data/lib/yobi/repository/cat.rb
CHANGED
|
@@ -4,6 +4,16 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Yobi
|
|
6
6
|
class Repository
|
|
7
|
+
# `restic cat config`: the repository's own config document.
|
|
8
|
+
#
|
|
9
|
+
# @return [Hash]
|
|
10
|
+
# @raise [Yobi::RepositoryNotFound, Yobi::AuthenticationFailed]
|
|
11
|
+
def cat_config
|
|
12
|
+
execution = run_restic(build_argv("cat", "config"))
|
|
13
|
+
JSON.parse(execution[:output].to_s)
|
|
14
|
+
end
|
|
15
|
+
alias_method :config, :cat_config
|
|
16
|
+
|
|
7
17
|
# `restic cat snapshot ID`: one snapshot's own raw stored record.
|
|
8
18
|
#
|
|
9
19
|
# @param id [String, Yobi::Snapshot]
|
|
@@ -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 check`: tests the repository for errors.
|
|
@@ -23,74 +20,93 @@ module Yobi
|
|
|
23
20
|
a.repeat_flag(:tag, tags)
|
|
24
21
|
a.flag(:with_cache) if with_cache
|
|
25
22
|
end
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
output = Yobi::ResticOutput.new(transform: Yobi::CheckMessageWrapper)
|
|
24
|
+
execution = run_restic(argv, output: output)
|
|
25
|
+
CheckOutcome.new(execution)
|
|
28
26
|
end
|
|
29
27
|
end
|
|
30
28
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
# Classifies a raw check message Hash by its own message_type, wrapping
|
|
30
|
+
# it in the matching typed class. Used both as the {Yobi::ResticOutput}
|
|
31
|
+
# `transform:` for {Yobi::Repository#check} and by {Yobi::CheckOutcome}'s
|
|
32
|
+
# own post-hoc accessors.
|
|
33
|
+
#
|
|
34
|
+
# @private
|
|
35
|
+
module CheckMessageWrapper
|
|
36
|
+
# @param raw [Hash]
|
|
37
|
+
# @return [Yobi::CheckError, Yobi::CheckSummary, Hash]
|
|
38
|
+
# the raw Hash itself for a message_type this version of Yobi doesn't recognize
|
|
39
|
+
def self.call(raw)
|
|
40
|
+
case raw["message_type"]
|
|
41
|
+
when "error" then CheckError.new(raw)
|
|
42
|
+
when "summary" then CheckSummary.new(raw)
|
|
43
|
+
else raw
|
|
44
|
+
end
|
|
36
45
|
end
|
|
46
|
+
end
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
# The outcome of one {Yobi::Repository#check} call.
|
|
49
|
+
class CheckOutcome
|
|
50
|
+
# @return [Yobi::ResticOutput]
|
|
51
|
+
attr_reader :output
|
|
42
52
|
|
|
43
|
-
private
|
|
53
|
+
# @private
|
|
54
|
+
def initialize(execution)
|
|
55
|
+
@output = execution[:output]
|
|
56
|
+
end
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if hash && hash["message_type"] == "summary"
|
|
49
|
-
hash
|
|
50
|
-
else
|
|
51
|
-
find_summary || {}
|
|
52
|
-
end
|
|
58
|
+
# @return [Yobi::CheckSummary] Restic's own `"summary"` fields
|
|
59
|
+
def summary
|
|
60
|
+
@summary ||= output.messages("summary").first || CheckSummary.new({})
|
|
53
61
|
end
|
|
62
|
+
alias_method :report, :summary
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
# @return [Enumerable<Yobi::CheckError>]
|
|
65
|
+
def errors
|
|
66
|
+
@errors ||= output.messages("error")
|
|
67
|
+
end
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
# @return [void]
|
|
70
|
+
def pretty_print(q)
|
|
71
|
+
q.object_group(self) do
|
|
72
|
+
q.breakable
|
|
73
|
+
q.text("(has errors)") if errors.any?
|
|
74
|
+
q.breakable
|
|
75
|
+
q.pp summary
|
|
61
76
|
end
|
|
62
|
-
|
|
63
|
-
nil
|
|
64
77
|
end
|
|
65
78
|
end
|
|
66
79
|
|
|
67
80
|
# One `"error"` message from a check run.
|
|
68
81
|
# https://restic.readthedocs.io/en/stable/075_scripting.html#error
|
|
69
|
-
class CheckError <
|
|
82
|
+
class CheckError < Yobi::FancyHash
|
|
70
83
|
# @return [String]
|
|
71
84
|
def message
|
|
72
85
|
self["message"]
|
|
73
86
|
end
|
|
74
87
|
end
|
|
75
88
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
# The `"summary"` message from a check run, the final result once the
|
|
90
|
+
# command finishes. Dispatched to {Yobi::CheckOutcome#summary}.
|
|
91
|
+
class CheckSummary < Yobi::FancyHash
|
|
92
|
+
# @return [Integer]
|
|
93
|
+
def num_errors
|
|
94
|
+
self["num_errors"] || 0
|
|
95
|
+
end
|
|
79
96
|
|
|
80
|
-
# @
|
|
81
|
-
def
|
|
82
|
-
|
|
97
|
+
# @return [Array<String>] pack IDs needing `repair_packs`/`repair_snapshots`
|
|
98
|
+
def broken_packs
|
|
99
|
+
self["broken_packs"] || []
|
|
83
100
|
end
|
|
84
101
|
|
|
85
|
-
# @
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
# @return [Boolean] whether to run {Yobi::Repository#repair_index}
|
|
103
|
+
def suggest_repair_index?
|
|
104
|
+
self["suggest_repair_index"]
|
|
105
|
+
end
|
|
89
106
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
end
|
|
107
|
+
# @return [Boolean] whether to run {Yobi::Repository#prune}
|
|
108
|
+
def suggest_prune?
|
|
109
|
+
self["suggest_prune"]
|
|
94
110
|
end
|
|
95
111
|
end
|
|
96
112
|
end
|
data/lib/yobi/repository/copy.rb
CHANGED
|
@@ -5,33 +5,31 @@ module Yobi
|
|
|
5
5
|
# `restic copy`: replicates snapshots from another repository into
|
|
6
6
|
# this one. Already-copied snapshots are skipped automatically.
|
|
7
7
|
#
|
|
8
|
-
# @param from_repo [String, Yobi::Repository] the source repository
|
|
9
|
-
#
|
|
8
|
+
# @param from_repo [String, Yobi::Repository, Array] the source repository's
|
|
9
|
+
# URL; a `Repository` instance (its own `#url`/`#password` are used
|
|
10
|
+
# automatically); or a `[:file, "..."]` tuple reading the URL from a file
|
|
10
11
|
# @param from_password [String, Array, Symbol, #call, nil] the source
|
|
11
12
|
# repository's password, same shape as {#initialize}'s `password:`
|
|
12
13
|
# @param snapshot_ids [Array<String>, String] snapshots to copy; all of them if empty
|
|
13
14
|
# @param from_key_hint [String, nil]
|
|
14
|
-
# @param from_repository_file [String, nil] read the source repository's URL from a file
|
|
15
15
|
# @param hosts [Array<String>, String] filter by hostname(s)
|
|
16
16
|
# @param paths [Array<String>, String] filter by originally backed-up path(s)
|
|
17
17
|
# @param tags [Array<String>, String] filter by tag(s)
|
|
18
18
|
# @return [true]
|
|
19
|
-
def copy(from_repo:, from_password: nil, snapshot_ids: [], from_key_hint: nil,
|
|
20
|
-
|
|
21
|
-
if
|
|
22
|
-
|
|
23
|
-
from_repo = from_repo.url
|
|
24
|
-
end
|
|
19
|
+
def copy(from_repo:, from_password: nil, snapshot_ids: [], from_key_hint: nil, hosts: [], paths: [], tags: [])
|
|
20
|
+
from_repo_flag, from_repo_value = resolved_from_repo_flag(from_repo)
|
|
21
|
+
from_password = from_repo_password(from_repo) if from_password.nil?
|
|
22
|
+
validate_from_password_shape!(from_password)
|
|
25
23
|
|
|
26
24
|
argv = build_argv("copy", snapshot_ids) do |a|
|
|
27
|
-
a.flag(
|
|
25
|
+
a.flag(from_repo_flag, from_repo_value)
|
|
28
26
|
a.flag(:from_insecure_no_password) if from_password == :insecure_no_password
|
|
29
27
|
a.flag(:from_key_hint, from_key_hint) unless from_key_hint.nil?
|
|
30
|
-
a.flag(:from_repository_file, from_repository_file) unless from_repository_file.nil?
|
|
31
28
|
a.repeat_flag(:host, hosts)
|
|
32
29
|
a.repeat_flag(:path, paths)
|
|
33
30
|
a.repeat_flag(:tag, tags)
|
|
34
31
|
end
|
|
32
|
+
|
|
35
33
|
run_restic(argv, extra_env: password_env(from_password, "RESTIC_FROM_PASSWORD"))
|
|
36
34
|
true
|
|
37
35
|
end
|