yobi 0.3.0 → 1.0.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/.ruby-version +1 -0
- data/CHANGELOG.md +17 -0
- data/README.md +42 -45
- data/lib/yobi/argv_builder.rb +1 -35
- data/lib/yobi/errors.rb +18 -30
- data/lib/yobi/fancy_hash.rb +1 -5
- data/lib/yobi/io_handle.rb +9 -17
- data/lib/yobi/mount_handle.rb +7 -11
- data/lib/yobi/repository/backup.rb +59 -90
- data/lib/yobi/repository/cat.rb +29 -51
- data/lib/yobi/repository/check.rb +21 -34
- data/lib/yobi/repository/copy.rb +11 -13
- data/lib/yobi/repository/diff.rb +18 -49
- data/lib/yobi/repository/dump.rb +14 -15
- data/lib/yobi/repository/find.rb +18 -39
- data/lib/yobi/repository/forget.rb +35 -41
- data/lib/yobi/repository/init.rb +20 -27
- data/lib/yobi/repository/key.rb +19 -34
- data/lib/yobi/repository/list.rb +5 -6
- data/lib/yobi/repository/ls.rb +20 -43
- data/lib/yobi/repository/migrate.rb +6 -6
- data/lib/yobi/repository/mount.rb +22 -32
- data/lib/yobi/repository/prune.rb +11 -9
- data/lib/yobi/repository/recover.rb +2 -4
- data/lib/yobi/repository/repair.rb +16 -23
- data/lib/yobi/repository/restore.rb +31 -54
- data/lib/yobi/repository/rewrite.rb +15 -20
- data/lib/yobi/repository/snapshots.rb +8 -9
- data/lib/yobi/repository/stats.rb +26 -23
- data/lib/yobi/repository/tag.rb +23 -33
- data/lib/yobi/repository/unlock.rb +2 -4
- data/lib/yobi/repository.rb +85 -16
- data/lib/yobi/restic.rb +73 -110
- data/lib/yobi/restic_output.rb +2 -74
- data/lib/yobi/snapshot.rb +15 -27
- data/lib/yobi/version.rb +1 -2
- data/sig/yobi.rbs +4 -4
- metadata +3 -2
data/lib/yobi/io_handle.rb
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
# A live Restic process streaming raw bytes to its own stdout, returned by
|
|
5
|
-
#
|
|
6
|
-
# contract (
|
|
5
|
+
# Yobi::Restic#run_dump when no block is given. Satisfies Rack's Body
|
|
6
|
+
# contract (+#each+, +#close+).
|
|
7
7
|
#
|
|
8
|
-
# Unlike the block form, nothing closes this automatically. Call
|
|
9
|
-
# yourself once done reading, in an
|
|
8
|
+
# Unlike the block form, nothing closes this automatically. Call #close
|
|
9
|
+
# yourself once done reading, in an +ensure+, so it still runs if reading
|
|
10
10
|
# raises:
|
|
11
11
|
#
|
|
12
|
-
# @example
|
|
13
12
|
# handle = repo.dump(snapshot_id: "latest", file: "/etc/hosts")
|
|
14
13
|
# begin
|
|
15
14
|
# IO.copy_stream(handle.io, "/tmp/hosts")
|
|
@@ -17,13 +16,12 @@ module Yobi
|
|
|
17
16
|
# handle.close
|
|
18
17
|
# end
|
|
19
18
|
class IOHandle
|
|
20
|
-
#
|
|
19
|
+
# The readable end of the pipe Restic's stdout is wired to.
|
|
21
20
|
attr_reader :io
|
|
22
|
-
#
|
|
21
|
+
# The Restic process's pid.
|
|
23
22
|
attr_reader :pid
|
|
24
23
|
|
|
25
|
-
#
|
|
26
|
-
def initialize(io, pid:, output:, argv:)
|
|
24
|
+
def initialize(io, pid:, output:, argv:) # :nodoc:
|
|
27
25
|
@io = io
|
|
28
26
|
@pid = pid
|
|
29
27
|
@output = output
|
|
@@ -31,20 +29,17 @@ module Yobi
|
|
|
31
29
|
@closed = false
|
|
32
30
|
end
|
|
33
31
|
|
|
34
|
-
#
|
|
32
|
+
# +true+ once #close has run.
|
|
35
33
|
def closed?
|
|
36
34
|
@closed
|
|
37
35
|
end
|
|
38
36
|
|
|
39
|
-
# @return [String]
|
|
40
37
|
def inspect
|
|
41
38
|
"#<#{self.class} pid=#{pid} closed=#{closed?}>"
|
|
42
39
|
end
|
|
43
40
|
|
|
44
41
|
# Reaps the Restic process and raises based on its exit code. Safe to
|
|
45
42
|
# call more than once.
|
|
46
|
-
#
|
|
47
|
-
# @return [void]
|
|
48
43
|
def close
|
|
49
44
|
return if @closed
|
|
50
45
|
|
|
@@ -54,10 +49,7 @@ module Yobi
|
|
|
54
49
|
Restic.dispatch(exit_code: status.exitstatus, output: @output, argv: @argv)
|
|
55
50
|
end
|
|
56
51
|
|
|
57
|
-
# Yields binary-safe chunks of
|
|
58
|
-
#
|
|
59
|
-
# @yieldparam chunk [String]
|
|
60
|
-
# @return [void]
|
|
52
|
+
# Yields binary-safe chunks of #io until EOF, then calls #close.
|
|
61
53
|
def each
|
|
62
54
|
loop do
|
|
63
55
|
yield @io.readpartial(64 * 1024)
|
data/lib/yobi/mount_handle.rb
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
|
-
# A live Restic mount process, returned by
|
|
4
|
+
# A live Restic mount process, returned by Yobi::Restic#run_mount once
|
|
5
5
|
# Restic has reported itself ready. The mounted filesystem itself is
|
|
6
|
-
# browsed with ordinary file I/O at
|
|
6
|
+
# browsed with ordinary file I/O at #mountpoint; this object only
|
|
7
7
|
# manages the Restic process's lifetime.
|
|
8
8
|
class MountHandle
|
|
9
|
-
#
|
|
9
|
+
# The path the repository is mounted at.
|
|
10
10
|
attr_reader :mountpoint
|
|
11
11
|
|
|
12
|
-
#
|
|
13
|
-
def initialize(wait_thr:, mountpoint:, pipe:, output:, argv:)
|
|
12
|
+
def initialize(wait_thr:, mountpoint:, pipe:, output:, argv:) # :nodoc:
|
|
14
13
|
@wait_thr = wait_thr
|
|
15
14
|
@mountpoint = mountpoint
|
|
16
15
|
@pipe = pipe
|
|
@@ -19,17 +18,16 @@ module Yobi
|
|
|
19
18
|
@stopped = false
|
|
20
19
|
end
|
|
21
20
|
|
|
22
|
-
#
|
|
21
|
+
# The Restic process's pid.
|
|
23
22
|
def pid
|
|
24
23
|
@wait_thr.pid
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
#
|
|
26
|
+
# +true+ once #stop has run.
|
|
28
27
|
def stopped?
|
|
29
28
|
@stopped
|
|
30
29
|
end
|
|
31
30
|
|
|
32
|
-
# @return [String]
|
|
33
31
|
def inspect
|
|
34
32
|
"#<#{self.class} pid=#{pid} mountpoint=#{mountpoint.inspect} stopped=#{stopped?}>"
|
|
35
33
|
end
|
|
@@ -37,9 +35,7 @@ module Yobi
|
|
|
37
35
|
# Sends the Restic process SIGINT, waits for it to unmount and exit,
|
|
38
36
|
# then raises based on its exit code. Safe to call more than once, and
|
|
39
37
|
# safe to call after the mount has already been stopped externally
|
|
40
|
-
# (e.g. via
|
|
41
|
-
#
|
|
42
|
-
# @return [void]
|
|
38
|
+
# (e.g. via +kill -INT+ or the OS's own +umount+/+fusermount+).
|
|
43
39
|
def stop
|
|
44
40
|
return if @stopped
|
|
45
41
|
|
|
@@ -5,41 +5,46 @@ require "shellwords"
|
|
|
5
5
|
|
|
6
6
|
module Yobi
|
|
7
7
|
class Repository
|
|
8
|
-
#
|
|
8
|
+
# +restic backup+: creates a new snapshot from a source path.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
10
|
+
# +source:+ is either a path to back up, or a
|
|
11
|
+
# +[:stdin_from_command, command]+ / +[:stdin_from_command, command, filename]+
|
|
12
|
+
# tuple. Restic spawns and executes +command+ itself (a String tokenized
|
|
13
|
+
# with +Shellwords.split+, or an Array of already-discrete arguments),
|
|
14
|
+
# capturing its stdout as the backup content.
|
|
15
|
+
#
|
|
16
|
+
# +excludes:+, +exclude_files:+, +exclude_if_present:+, +iexcludes:+ and
|
|
17
|
+
# +iexclude_files:+ each accept a single value or an Array. +i+-prefixed
|
|
18
|
+
# variants are case-insensitive.
|
|
19
|
+
#
|
|
20
|
+
# +exclude_larger_than:+ skips files larger than a given size (e.g.
|
|
21
|
+
# +"1G"+). +exclude_caches:+ skips directories containing a
|
|
22
|
+
# +CACHEDIR.TAG+ marker. +exclude_cloud_files:+ skips files not fully
|
|
23
|
+
# present on disk (e.g. OneDrive placeholders).
|
|
24
|
+
#
|
|
25
|
+
# +files_from:+, +files_from_raw:+ and +files_from_verbatim:+ read the
|
|
26
|
+
# files/dirs to back up from a file, one per line - +_raw+ NUL-separated,
|
|
27
|
+
# +_verbatim+ taken literally with no glob expansion.
|
|
28
|
+
#
|
|
29
|
+
# +tags:+ attaches tags to the new snapshot. +host:+ overrides the OS
|
|
30
|
+
# hostname recorded on it. +time:+ overrides its creation timestamp.
|
|
31
|
+
# +parent:+ pins the parent snapshot ID; +group_by:+ (e.g. +"host,paths"+)
|
|
32
|
+
# picks the grouping used to find the parent otherwise.
|
|
33
|
+
#
|
|
34
|
+
# +dry_run:+ reports what would happen without doing it. +force:+ backs
|
|
35
|
+
# up unchanged files instead of skipping them. +skip_if_unchanged:+
|
|
36
|
+
# doesn't create a snapshot if nothing changed. +ignore_ctime:+ and
|
|
37
|
+
# +ignore_inode:+ relax change detection. +with_atime:+ also stores
|
|
38
|
+
# files' access times.
|
|
39
|
+
#
|
|
40
|
+
# +one_file_system:+ doesn't cross filesystem boundaries. +no_scan:+
|
|
41
|
+
# skips the pre-backup scan (which disables percentage progress).
|
|
42
|
+
# +read_concurrency:+ sets the number of concurrent file reads.
|
|
43
|
+
#
|
|
44
|
+
# When +verbose: true+, a Yobi::BackupVerboseStatus is streamed to the
|
|
45
|
+
# block per file. The block also receives Yobi::BackupStatus,
|
|
46
|
+
# Yobi::BackupError, and Yobi::BackupSummary messages as they arrive.
|
|
47
|
+
# Returns a Yobi::BackupOutcome.
|
|
43
48
|
def backup(source:, excludes: [], exclude_files: [], exclude_if_present: [], exclude_larger_than: nil,
|
|
44
49
|
files_from: [], files_from_raw: [], files_from_verbatim: [], iexcludes: [], iexclude_files: [],
|
|
45
50
|
tags: [], dry_run: false, exclude_caches: false, exclude_cloud_files: false, force: false,
|
|
@@ -107,16 +112,7 @@ module Yobi
|
|
|
107
112
|
end
|
|
108
113
|
end
|
|
109
114
|
|
|
110
|
-
|
|
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
|
|
115
|
+
module BackupMessageWrapper # :nodoc:
|
|
120
116
|
def self.call(raw)
|
|
121
117
|
case raw["message_type"]
|
|
122
118
|
when "status" then BackupStatus.new(raw)
|
|
@@ -128,36 +124,31 @@ module Yobi
|
|
|
128
124
|
end
|
|
129
125
|
end
|
|
130
126
|
|
|
131
|
-
# The outcome of one
|
|
127
|
+
# The outcome of one Yobi::Repository#backup call.
|
|
132
128
|
class BackupOutcome
|
|
133
|
-
#
|
|
134
|
-
COMMAND_OUTPUT_LINE_PATTERN = /\Asubprocess [^:]+: (.*)/
|
|
129
|
+
COMMAND_OUTPUT_LINE_PATTERN = /\Asubprocess [^:]+: (.*)/ # :nodoc:
|
|
135
130
|
|
|
136
|
-
#
|
|
131
|
+
# The Yobi::ResticOutput backing this outcome.
|
|
137
132
|
attr_reader :output
|
|
138
133
|
|
|
139
|
-
#
|
|
140
|
-
def initialize(execution)
|
|
134
|
+
def initialize(execution) # :nodoc:
|
|
141
135
|
@output = execution[:output]
|
|
142
136
|
end
|
|
143
137
|
|
|
144
|
-
#
|
|
138
|
+
# Every Yobi::BackupError message from the run.
|
|
145
139
|
def errors
|
|
146
140
|
@errors ||= output.messages("error")
|
|
147
141
|
end
|
|
148
142
|
|
|
149
143
|
# Every message from the run, in file order, each wrapped in its own
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
# @return [Enumerable<Yobi::BackupStatus, Yobi::BackupError, Yobi::BackupVerboseStatus, Yobi::BackupSummary>]
|
|
144
|
+
# Yobi::BackupStatus / Yobi::BackupError / Yobi::BackupVerboseStatus /
|
|
145
|
+
# Yobi::BackupSummary.
|
|
153
146
|
def messages
|
|
154
147
|
@messages ||= output.messages
|
|
155
148
|
end
|
|
156
149
|
|
|
157
|
-
# The
|
|
158
|
-
#
|
|
159
|
-
# @yieldparam line [String]
|
|
160
|
-
# @return [Enumerator] if no block is given
|
|
150
|
+
# The +source: [:stdin_from_command, ...]+ subprocess's own stderr, if
|
|
151
|
+
# any, de-prefixed.
|
|
161
152
|
def command_output
|
|
162
153
|
return enum_for(:command_output) unless block_given?
|
|
163
154
|
|
|
@@ -167,13 +158,12 @@ module Yobi
|
|
|
167
158
|
end
|
|
168
159
|
end
|
|
169
160
|
|
|
170
|
-
#
|
|
161
|
+
# The Yobi::BackupSummary of Restic's own +"summary"+ fields.
|
|
171
162
|
def summary
|
|
172
163
|
@summary ||= output.messages("summary").first || BackupSummary.new({})
|
|
173
164
|
end
|
|
174
165
|
alias_method :report, :summary
|
|
175
166
|
|
|
176
|
-
# @return [void]
|
|
177
167
|
def pretty_print(q)
|
|
178
168
|
q.object_group(self) do
|
|
179
169
|
q.breakable
|
|
@@ -184,119 +174,98 @@ module Yobi
|
|
|
184
174
|
end
|
|
185
175
|
end
|
|
186
176
|
|
|
187
|
-
# The
|
|
188
|
-
# command finishes. Dispatched to
|
|
189
|
-
# also what
|
|
190
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#summary
|
|
177
|
+
# The +"summary"+ message from a backup run, the final result once the
|
|
178
|
+
# command finishes. Dispatched to Yobi::Repository#backup's block, and
|
|
179
|
+
# also what Yobi::BackupOutcome#summary returns. See
|
|
180
|
+
# https://restic.readthedocs.io/en/stable/075_scripting.html#summary.
|
|
191
181
|
class BackupSummary < Yobi::FancyHash
|
|
192
|
-
# @return [Time, nil]
|
|
193
182
|
def backup_start
|
|
194
183
|
@backup_start ||= Time.parse(self["backup_start"]) if self["backup_start"]
|
|
195
184
|
end
|
|
196
185
|
|
|
197
|
-
# @return [Time, nil]
|
|
198
186
|
def backup_end
|
|
199
187
|
@backup_end ||= Time.parse(self["backup_end"]) if self["backup_end"]
|
|
200
188
|
end
|
|
201
189
|
end
|
|
202
190
|
|
|
203
|
-
# One
|
|
204
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#status
|
|
191
|
+
# One +"status"+ message from a live backup run. See
|
|
192
|
+
# https://restic.readthedocs.io/en/stable/075_scripting.html#status.
|
|
205
193
|
class BackupStatus < Yobi::FancyHash
|
|
206
|
-
# @return [Float, nil]
|
|
207
194
|
def percent_done
|
|
208
195
|
self["percent_done"]
|
|
209
196
|
end
|
|
210
197
|
|
|
211
|
-
# @return [Integer, nil]
|
|
212
198
|
def total_files
|
|
213
199
|
self["total_files"]
|
|
214
200
|
end
|
|
215
201
|
|
|
216
|
-
# @return [Integer, nil]
|
|
217
202
|
def files_done
|
|
218
203
|
self["files_done"]
|
|
219
204
|
end
|
|
220
205
|
|
|
221
|
-
# @return [Integer, nil]
|
|
222
206
|
def total_bytes
|
|
223
207
|
self["total_bytes"]
|
|
224
208
|
end
|
|
225
209
|
|
|
226
|
-
# @return [Integer, nil]
|
|
227
210
|
def bytes_done
|
|
228
211
|
self["bytes_done"]
|
|
229
212
|
end
|
|
230
213
|
|
|
231
|
-
# @return [Array<String>]
|
|
232
214
|
def current_files
|
|
233
215
|
self["current_files"] || []
|
|
234
216
|
end
|
|
235
217
|
|
|
236
|
-
# @return [Integer]
|
|
237
218
|
def error_count
|
|
238
219
|
self["error_count"] || 0
|
|
239
220
|
end
|
|
240
221
|
end
|
|
241
222
|
|
|
242
|
-
# One
|
|
243
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#error
|
|
223
|
+
# One +"error"+ message from a backup run.
|
|
244
224
|
class BackupError < Yobi::FancyHash
|
|
245
|
-
# @return [String, nil]
|
|
246
225
|
def message
|
|
247
226
|
dig("error", "message")
|
|
248
227
|
end
|
|
249
228
|
|
|
250
|
-
# @return [String, nil]
|
|
251
229
|
def during
|
|
252
230
|
self["during"]
|
|
253
231
|
end
|
|
254
232
|
|
|
255
|
-
# @return [String, nil]
|
|
256
233
|
def item
|
|
257
234
|
self["item"]
|
|
258
235
|
end
|
|
259
236
|
end
|
|
260
237
|
|
|
261
|
-
# One
|
|
262
|
-
# emitted when
|
|
238
|
+
# One +"verbose_status"+ message from a backup run, one per file. Only
|
|
239
|
+
# emitted when +verbose: true+ is passed to Yobi::Repository#backup.
|
|
263
240
|
class BackupVerboseStatus < Yobi::FancyHash
|
|
264
|
-
# @return [String]
|
|
265
241
|
def action
|
|
266
242
|
self["action"]
|
|
267
243
|
end
|
|
268
244
|
|
|
269
|
-
# @return [String]
|
|
270
245
|
def item
|
|
271
246
|
self["item"]
|
|
272
247
|
end
|
|
273
248
|
|
|
274
|
-
# @return [Integer]
|
|
275
249
|
def duration
|
|
276
250
|
self["duration"]
|
|
277
251
|
end
|
|
278
252
|
|
|
279
|
-
# @return [Integer]
|
|
280
253
|
def data_size
|
|
281
254
|
self["data_size"]
|
|
282
255
|
end
|
|
283
256
|
|
|
284
|
-
# @return [Integer]
|
|
285
257
|
def data_size_in_repo
|
|
286
258
|
self["data_size_in_repo"]
|
|
287
259
|
end
|
|
288
260
|
|
|
289
|
-
# @return [Integer]
|
|
290
261
|
def metadata_size
|
|
291
262
|
self["metadata_size"]
|
|
292
263
|
end
|
|
293
264
|
|
|
294
|
-
# @return [Integer]
|
|
295
265
|
def metadata_size_in_repo
|
|
296
266
|
self["metadata_size_in_repo"]
|
|
297
267
|
end
|
|
298
268
|
|
|
299
|
-
# @return [Integer]
|
|
300
269
|
def total_files
|
|
301
270
|
self["total_files"]
|
|
302
271
|
end
|
data/lib/yobi/repository/cat.rb
CHANGED
|
@@ -1,55 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Yobi
|
|
6
4
|
class Repository
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @return [Hash]
|
|
10
|
-
# @raise [Yobi::RepositoryNotFound, Yobi::AuthenticationFailed]
|
|
5
|
+
# +restic cat config+: the repository's own config document, as a Hash.
|
|
6
|
+
# Raises Yobi::RepositoryNotFound or Yobi::AuthenticationFailed.
|
|
11
7
|
def cat_config
|
|
12
8
|
execution = run_restic(build_argv("cat", "config"))
|
|
13
|
-
|
|
9
|
+
parse_json_output(execution)
|
|
14
10
|
end
|
|
15
11
|
alias_method :config, :cat_config
|
|
16
12
|
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# @param id [String, Yobi::Snapshot]
|
|
20
|
-
# @return [Hash]
|
|
13
|
+
# +restic cat snapshot ID+: one snapshot's own raw stored record, as a
|
|
14
|
+
# Hash. +id+ can be a String or a Yobi::Snapshot.
|
|
21
15
|
def cat_snapshot(id)
|
|
22
16
|
id = id.id if id.is_a?(Yobi::Snapshot)
|
|
23
17
|
execution = run_restic(build_argv("cat", "snapshot", id))
|
|
24
|
-
|
|
18
|
+
parse_json_output(execution)
|
|
25
19
|
end
|
|
26
20
|
|
|
27
|
-
#
|
|
28
|
-
# from
|
|
29
|
-
#
|
|
30
|
-
# @param id [String]
|
|
31
|
-
# @return [Hash]
|
|
21
|
+
# +restic cat index ID+: one index file's own raw contents, as a Hash.
|
|
22
|
+
# IDs come from +list(:index)+.
|
|
32
23
|
def cat_index(id)
|
|
33
24
|
execution = run_restic(build_argv("cat", "index", id))
|
|
34
|
-
|
|
25
|
+
parse_json_output(execution)
|
|
35
26
|
end
|
|
36
27
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# @param id [String, Yobi::Key]
|
|
40
|
-
# @return [Hash]
|
|
28
|
+
# +restic cat key ID+: one key's own raw stored record, as a Hash. +id+
|
|
29
|
+
# can be a String or a Yobi::Key.
|
|
41
30
|
def cat_key(id)
|
|
42
31
|
id = id.id if id.is_a?(Yobi::Key)
|
|
43
32
|
execution = run_restic(build_argv("cat", "key", id))
|
|
44
|
-
|
|
33
|
+
parse_json_output(execution)
|
|
45
34
|
end
|
|
46
35
|
|
|
47
|
-
#
|
|
48
|
-
# snapshot's root, or at
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @param subfolder [String, nil]
|
|
52
|
-
# @return [Hash]
|
|
36
|
+
# +restic cat tree snapshot:subfolder+: the raw tree object at a
|
|
37
|
+
# snapshot's root, or at +subfolder+ within it. +snapshot_id+ can be a
|
|
38
|
+
# String, a Yobi::Snapshot, or Restic's own +"snapshotID:subfolder"+
|
|
39
|
+
# form directly.
|
|
53
40
|
def cat_tree(snapshot_id, subfolder: nil)
|
|
54
41
|
snapshot_id = snapshot_id.id if snapshot_id.is_a?(Yobi::Snapshot)
|
|
55
42
|
target = if subfolder.nil?
|
|
@@ -58,40 +45,31 @@ module Yobi
|
|
|
58
45
|
"#{snapshot_id}:#{subfolder}"
|
|
59
46
|
end
|
|
60
47
|
execution = run_restic(build_argv("cat", "tree", target))
|
|
61
|
-
|
|
48
|
+
parse_json_output(execution)
|
|
62
49
|
end
|
|
63
50
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# @param id [String]
|
|
68
|
-
# @yieldparam io [IO]
|
|
69
|
-
# @return [Yobi::IOHandle] if no block is given
|
|
51
|
+
# +restic cat pack ID+: one pack file's raw, still-encrypted bytes. IDs
|
|
52
|
+
# come from +list(:packs)+. Same block/handle shape as #dump: without a
|
|
53
|
+
# block, returns a Yobi::IOHandle.
|
|
70
54
|
def cat_pack(id, &block)
|
|
71
55
|
run_restic_dump(build_argv("cat", "pack", id), &block)
|
|
72
56
|
end
|
|
73
57
|
|
|
74
|
-
#
|
|
75
|
-
# from
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# @param id [String]
|
|
79
|
-
# @yieldparam io [IO]
|
|
80
|
-
# @return [Yobi::IOHandle] if no block is given
|
|
58
|
+
# +restic cat blob ID+: one data blob's raw, decrypted bytes. IDs come
|
|
59
|
+
# from #cat_tree's own node +"content"+ arrays. Same block/handle shape
|
|
60
|
+
# as #dump.
|
|
81
61
|
def cat_blob(id, &block)
|
|
82
62
|
run_restic_dump(build_argv("cat", "blob", id), &block)
|
|
83
63
|
end
|
|
84
64
|
|
|
85
|
-
#
|
|
86
|
-
# material. Extremely sensitive: this is the actual key, not a
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# @return [Hash]
|
|
65
|
+
# +restic cat masterkey+: this repository's own encryption/MAC key
|
|
66
|
+
# material. Extremely sensitive: this is the actual key, not a redacted
|
|
67
|
+
# reference to it, and there is no operation that rotates it; every
|
|
68
|
+
# other key/password management method here only manages different ways
|
|
69
|
+
# to unlock this same master key.
|
|
92
70
|
def cat_masterkey_and_game_over_if_this_leaks
|
|
93
71
|
execution = run_restic(build_argv("cat", "masterkey"))
|
|
94
|
-
|
|
72
|
+
parse_json_output(execution)
|
|
95
73
|
end
|
|
96
74
|
end
|
|
97
75
|
end
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
5
|
+
# +restic check+: tests the repository for errors.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
7
|
+
# +hosts:+, +paths:+ and +tags:+ each accept a single value or an Array
|
|
8
|
+
# to filter by.
|
|
9
|
+
#
|
|
10
|
+
# +read_data:+ reads and verifies pack file contents, not just structure.
|
|
11
|
+
# +read_data_subset:+ narrows that to a subset of packs, e.g. +"5%"+.
|
|
12
|
+
# +with_cache:+ uses the local cache.
|
|
13
|
+
#
|
|
14
|
+
# Returns a Yobi::CheckOutcome.
|
|
14
15
|
def check(hosts: [], paths: [], read_data: false, read_data_subset: nil, tags: [], with_cache: false)
|
|
15
16
|
argv = build_argv("check") do |a|
|
|
16
17
|
a.repeat_flag(:host, hosts)
|
|
@@ -26,16 +27,7 @@ module Yobi
|
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
|
|
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
|
|
30
|
+
module CheckMessageWrapper # :nodoc:
|
|
39
31
|
def self.call(raw)
|
|
40
32
|
case raw["message_type"]
|
|
41
33
|
when "error" then CheckError.new(raw)
|
|
@@ -45,28 +37,26 @@ module Yobi
|
|
|
45
37
|
end
|
|
46
38
|
end
|
|
47
39
|
|
|
48
|
-
# The outcome of one
|
|
40
|
+
# The outcome of one Yobi::Repository#check call.
|
|
49
41
|
class CheckOutcome
|
|
50
|
-
#
|
|
42
|
+
# The Yobi::ResticOutput backing this outcome.
|
|
51
43
|
attr_reader :output
|
|
52
44
|
|
|
53
|
-
#
|
|
54
|
-
def initialize(execution)
|
|
45
|
+
def initialize(execution) # :nodoc:
|
|
55
46
|
@output = execution[:output]
|
|
56
47
|
end
|
|
57
48
|
|
|
58
|
-
#
|
|
49
|
+
# The Yobi::CheckSummary of Restic's own +"summary"+ fields.
|
|
59
50
|
def summary
|
|
60
51
|
@summary ||= output.messages("summary").first || CheckSummary.new({})
|
|
61
52
|
end
|
|
62
53
|
alias_method :report, :summary
|
|
63
54
|
|
|
64
|
-
#
|
|
55
|
+
# Every Yobi::CheckError from the run.
|
|
65
56
|
def errors
|
|
66
57
|
@errors ||= output.messages("error")
|
|
67
58
|
end
|
|
68
59
|
|
|
69
|
-
# @return [void]
|
|
70
60
|
def pretty_print(q)
|
|
71
61
|
q.object_group(self) do
|
|
72
62
|
q.breakable
|
|
@@ -77,34 +67,31 @@ module Yobi
|
|
|
77
67
|
end
|
|
78
68
|
end
|
|
79
69
|
|
|
80
|
-
# One
|
|
81
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#error
|
|
70
|
+
# One +"error"+ message from a check run.
|
|
82
71
|
class CheckError < Yobi::FancyHash
|
|
83
|
-
# @return [String]
|
|
84
72
|
def message
|
|
85
73
|
self["message"]
|
|
86
74
|
end
|
|
87
75
|
end
|
|
88
76
|
|
|
89
|
-
# The
|
|
90
|
-
# command finishes. Dispatched to
|
|
77
|
+
# The +"summary"+ message from a check run, the final result once the
|
|
78
|
+
# command finishes. Dispatched to Yobi::CheckOutcome#summary.
|
|
91
79
|
class CheckSummary < Yobi::FancyHash
|
|
92
|
-
# @return [Integer]
|
|
93
80
|
def num_errors
|
|
94
81
|
self["num_errors"] || 0
|
|
95
82
|
end
|
|
96
83
|
|
|
97
|
-
#
|
|
84
|
+
# Pack IDs needing #repair_packs / #repair_snapshots.
|
|
98
85
|
def broken_packs
|
|
99
86
|
self["broken_packs"] || []
|
|
100
87
|
end
|
|
101
88
|
|
|
102
|
-
#
|
|
89
|
+
# Whether to run Yobi::Repository#repair_index.
|
|
103
90
|
def suggest_repair_index?
|
|
104
91
|
self["suggest_repair_index"]
|
|
105
92
|
end
|
|
106
93
|
|
|
107
|
-
#
|
|
94
|
+
# Whether to run Yobi::Repository#prune.
|
|
108
95
|
def suggest_prune?
|
|
109
96
|
self["suggest_prune"]
|
|
110
97
|
end
|