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/restic.rb
CHANGED
|
@@ -7,87 +7,68 @@ module Yobi
|
|
|
7
7
|
# A specific Restic binary, plus its process-level settings that apply
|
|
8
8
|
# regardless of which repository is being operated on.
|
|
9
9
|
# Repository-specific identity (url, credentials) lives on
|
|
10
|
-
#
|
|
10
|
+
# Yobi::Repository instead.
|
|
11
11
|
class Restic
|
|
12
|
-
# The oldest Restic version
|
|
12
|
+
# The oldest Restic version #run/#run_dump/#run_mount verify the
|
|
13
13
|
# installed binary meets before executing a real command. See
|
|
14
|
-
#
|
|
15
|
-
MINIMUM_VERSION = Gem::Version.new("0.
|
|
14
|
+
# Yobi::UnsupportedResticVersion.
|
|
15
|
+
MINIMUM_VERSION = Gem::Version.new("0.18.0") # :nodoc:
|
|
16
16
|
|
|
17
|
-
# Env vars
|
|
18
|
-
ALLOWED_ENV_VARS = %w[
|
|
17
|
+
# Env vars #inspect shows in the clear; anything else is redacted.
|
|
18
|
+
ALLOWED_ENV_VARS = %w[ # :nodoc:
|
|
19
19
|
RESTIC_REPOSITORY RESTIC_CACHE_DIR RESTIC_COMPRESSION RESTIC_PACK_SIZE
|
|
20
20
|
RESTIC_READ_CONCURRENCY RESTIC_HOST RESTIC_PROGRESS_FPS RESTIC_CACERT
|
|
21
21
|
RESTIC_TLS_CLIENT_CERT RESTIC_KEY_HINT TMPDIR TMP AWS_DEFAULT_REGION
|
|
22
22
|
AWS_SHARED_CREDENTIALS_FILE AZURE_ENDPOINT_SUFFIX AZURE_FORCE_CLI_CREDENTIAL
|
|
23
|
-
RCLONE_BWLIMIT
|
|
24
|
-
].to_set.freeze
|
|
23
|
+
RCLONE_BWLIMIT].to_set.freeze
|
|
25
24
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
#
|
|
25
|
+
# Path to the Restic binary.
|
|
26
|
+
attr_accessor :restic_path
|
|
27
|
+
# +$RESTIC_CACHE_DIR+
|
|
29
28
|
attr_accessor :cache_dir
|
|
30
|
-
#
|
|
29
|
+
# +$RESTIC_COMPRESSION+
|
|
31
30
|
attr_accessor :compression
|
|
32
|
-
#
|
|
31
|
+
# +$RESTIC_PACK_SIZE+
|
|
33
32
|
attr_accessor :pack_size
|
|
34
|
-
#
|
|
33
|
+
# +$RESTIC_READ_CONCURRENCY+
|
|
35
34
|
attr_accessor :read_concurrency
|
|
36
|
-
#
|
|
35
|
+
# +$RESTIC_HOST+
|
|
37
36
|
attr_accessor :host
|
|
38
|
-
#
|
|
37
|
+
# +$RESTIC_PROGRESS_FPS+
|
|
39
38
|
attr_accessor :progress_fps
|
|
40
|
-
#
|
|
39
|
+
# +$RESTIC_CACERT+
|
|
41
40
|
attr_accessor :cacert
|
|
42
|
-
#
|
|
41
|
+
# +$RESTIC_TLS_CLIENT_CERT+
|
|
43
42
|
attr_accessor :tls_client_cert
|
|
44
|
-
#
|
|
43
|
+
# +$RESTIC_KEY_HINT+
|
|
45
44
|
attr_accessor :key_hint
|
|
46
|
-
#
|
|
45
|
+
# +--limit-download+
|
|
47
46
|
attr_accessor :limit_download
|
|
48
|
-
#
|
|
47
|
+
# +--limit-upload+
|
|
49
48
|
attr_accessor :limit_upload
|
|
50
|
-
#
|
|
49
|
+
# +--retry-lock+
|
|
51
50
|
attr_accessor :retry_lock
|
|
52
|
-
#
|
|
51
|
+
# +--no-lock+
|
|
53
52
|
attr_accessor :no_lock
|
|
54
|
-
#
|
|
53
|
+
# +--no-cache+
|
|
55
54
|
attr_accessor :no_cache
|
|
56
|
-
#
|
|
55
|
+
# +--cleanup-cache+
|
|
57
56
|
attr_accessor :cleanup_cache
|
|
58
|
-
#
|
|
57
|
+
# +--no-extra-verify+
|
|
59
58
|
attr_accessor :no_extra_verify
|
|
60
|
-
#
|
|
59
|
+
# +--stuck-request-timeout+
|
|
61
60
|
attr_accessor :stuck_request_timeout
|
|
62
|
-
#
|
|
61
|
+
# +--option+, one per element.
|
|
63
62
|
attr_accessor :options
|
|
64
|
-
#
|
|
63
|
+
# +--http-user-agent+
|
|
65
64
|
attr_accessor :http_user_agent
|
|
66
|
-
#
|
|
65
|
+
# +--quiet+
|
|
67
66
|
attr_accessor :quiet
|
|
68
67
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
# @param pack_size [Integer, nil]
|
|
74
|
-
# @param read_concurrency [Integer, nil]
|
|
75
|
-
# @param host [String, nil]
|
|
76
|
-
# @param progress_fps [Integer, nil]
|
|
77
|
-
# @param cacert [String, nil]
|
|
78
|
-
# @param tls_client_cert [String, nil]
|
|
79
|
-
# @param key_hint [String, nil]
|
|
80
|
-
# @param limit_download [String, nil]
|
|
81
|
-
# @param limit_upload [String, nil]
|
|
82
|
-
# @param retry_lock [String, nil]
|
|
83
|
-
# @param no_lock [Boolean]
|
|
84
|
-
# @param no_cache [Boolean]
|
|
85
|
-
# @param cleanup_cache [Boolean]
|
|
86
|
-
# @param no_extra_verify [Boolean]
|
|
87
|
-
# @param stuck_request_timeout [String, nil]
|
|
88
|
-
# @param options [Array<String>]
|
|
89
|
-
# @param http_user_agent [String, nil]
|
|
90
|
-
# @param quiet [Boolean]
|
|
68
|
+
# Builds a Restic executor. +restic_path+ defaults to +$RESTIC_PATH+,
|
|
69
|
+
# then +"restic"+. +env:+ is extra env vars merged over the named
|
|
70
|
+
# settings below (env-var-backed accessors like +cache_dir:+,
|
|
71
|
+
# +compression:+, etc.).
|
|
91
72
|
def initialize(restic_path = nil, env: {}, cache_dir: nil, compression: nil, pack_size: nil,
|
|
92
73
|
read_concurrency: nil, host: nil, progress_fps: nil, cacert: nil, tls_client_cert: nil,
|
|
93
74
|
key_hint: nil, limit_download: nil, limit_upload: nil, retry_lock: nil, no_lock: false,
|
|
@@ -118,10 +99,8 @@ module Yobi
|
|
|
118
99
|
end
|
|
119
100
|
|
|
120
101
|
# The env-var-backed settings above, computed fresh from their current
|
|
121
|
-
# accessor values on every call.
|
|
102
|
+
# accessor values on every call. +env:+ given at construction wins over
|
|
122
103
|
# any of these on key collision.
|
|
123
|
-
#
|
|
124
|
-
# @return [Hash{String => String}]
|
|
125
104
|
def env
|
|
126
105
|
{
|
|
127
106
|
"RESTIC_CACHE_DIR" => cache_dir,
|
|
@@ -137,12 +116,9 @@ module Yobi
|
|
|
137
116
|
end
|
|
138
117
|
|
|
139
118
|
# Appends the CLI-only global flags (the ones above with no env var
|
|
140
|
-
# equivalent) to a builder. Called from both
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# @param a [Yobi::ArgvBuilder]
|
|
144
|
-
# @return [void]
|
|
145
|
-
def append_global_flags(a)
|
|
119
|
+
# equivalent) to a builder. Called from both #build_argv and
|
|
120
|
+
# Repository#build_argv.
|
|
121
|
+
def append_global_flags(a) # :nodoc:
|
|
146
122
|
a.flag(:limit_download, limit_download) unless limit_download.nil?
|
|
147
123
|
a.flag(:limit_upload, limit_upload) unless limit_upload.nil?
|
|
148
124
|
a.flag(:retry_lock, retry_lock) unless retry_lock.nil?
|
|
@@ -156,26 +132,22 @@ module Yobi
|
|
|
156
132
|
a.flag(:quiet) if quiet
|
|
157
133
|
end
|
|
158
134
|
|
|
159
|
-
#
|
|
135
|
+
# Redacts sensitive env values so a stray +pp+/+puts+/log call never
|
|
136
|
+
# prints a credential in plaintext.
|
|
160
137
|
def inspect
|
|
161
138
|
"#<#{self.class} restic_path=#{restic_path.inspect} env=#{redacted_env.inspect}>"
|
|
162
139
|
end
|
|
163
140
|
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# @return [Yobi::ResticVersion]
|
|
141
|
+
# +restic version+: the installed binary's own version info. Returns a
|
|
142
|
+
# Yobi::ResticVersion.
|
|
167
143
|
def version
|
|
168
144
|
execution = run(build_argv("version"), skip_version_check: true)
|
|
169
145
|
Yobi::ResticVersion.new(parse_version_output(execution[:output].to_s))
|
|
170
146
|
end
|
|
171
147
|
|
|
172
|
-
#
|
|
173
|
-
# Not repository-scoped.
|
|
174
|
-
#
|
|
175
|
-
# @param cleanup [Boolean] `--cleanup`
|
|
176
|
-
# @param max_age [String, nil] `--max-age`
|
|
177
|
-
# @param no_size [Boolean] `--no-size`
|
|
178
|
-
# @return [true]
|
|
148
|
+
# +restic cache+: lists and optionally cleans local cache directories.
|
|
149
|
+
# Not repository-scoped. +cleanup:+ triggers +--cleanup+, +max_age:+
|
|
150
|
+
# sets +--max-age+, +no_size:+ triggers +--no-size+. Returns +true+.
|
|
179
151
|
def cache(cleanup: false, max_age: nil, no_size: false)
|
|
180
152
|
argv = build_argv("cache") do |a|
|
|
181
153
|
a.flag(:cleanup) if cleanup
|
|
@@ -186,19 +158,19 @@ module Yobi
|
|
|
186
158
|
true
|
|
187
159
|
end
|
|
188
160
|
|
|
189
|
-
# Runs argv against this Restic binary, merging extra_env with
|
|
190
|
-
# instance's own global env.
|
|
161
|
+
# Runs +argv+ against this Restic binary, merging +extra_env:+ with
|
|
162
|
+
# this instance's own global env. +output:+ takes a caller-configured
|
|
163
|
+
# Yobi::ResticOutput (e.g. with its own +transform:+); passing one forces
|
|
164
|
+
# streaming even without a block. When a block is given, each parsed
|
|
165
|
+
# message is yielded live as the command runs.
|
|
191
166
|
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
# @return [Hash] `{exit_code:, output:, argv:}` on exit code 0/3
|
|
200
|
-
# @raise [Yobi::RepositoryNotFound, Yobi::RepositoryLocked, Yobi::AuthenticationFailed, Yobi::ResticCommandFailed]
|
|
201
|
-
def run(argv, extra_env: {}, skip_version_check: false, output: nil, &block)
|
|
167
|
+
# +skip_version_check:+ is used internally by #version to avoid recursing
|
|
168
|
+
# into #ensure_minimum_version!
|
|
169
|
+
#
|
|
170
|
+
# Returns +{exit_code:, output:, argv:}+ on exit code 0 or 3.
|
|
171
|
+
# Raises Yobi::RepositoryNotFound, Yobi::RepositoryLocked,
|
|
172
|
+
# Yobi::AuthenticationFailed, or Yobi::ResticCommandFailed otherwise.
|
|
173
|
+
def run(argv, extra_env: {}, skip_version_check: false, output: nil, &block) # :nodoc:
|
|
202
174
|
ensure_minimum_version! unless skip_version_check
|
|
203
175
|
execution = if output || block
|
|
204
176
|
execute_with_streaming(argv, extra_env, output: output, &block)
|
|
@@ -208,10 +180,9 @@ module Yobi
|
|
|
208
180
|
self.class.dispatch(execution)
|
|
209
181
|
end
|
|
210
182
|
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
def self.dispatch(execution)
|
|
183
|
+
# Maps Restic's exit codes to Yobi's typed errors. Returns +execution+
|
|
184
|
+
# unchanged on 0/3.
|
|
185
|
+
def self.dispatch(execution) # :nodoc:
|
|
215
186
|
case execution[:exit_code]
|
|
216
187
|
when 0, 3
|
|
217
188
|
execution
|
|
@@ -227,21 +198,15 @@ module Yobi
|
|
|
227
198
|
end
|
|
228
199
|
|
|
229
200
|
# For commands whose success output is raw bytes with no JSON message
|
|
230
|
-
# framing (currently only
|
|
231
|
-
#
|
|
201
|
+
# framing (currently only Repository#dump). Spawns Restic with stdout
|
|
202
|
+
# wired to a pipe.
|
|
232
203
|
#
|
|
233
|
-
# Without a block, returns a
|
|
204
|
+
# Without a block, returns a Yobi::IOHandle immediately; closing,
|
|
234
205
|
# reaping, and exit-code dispatch are the caller's own responsibility.
|
|
235
206
|
# With one, yields the pipe's read end; it's always closed and the
|
|
236
207
|
# process always reaped once the block returns or raises, before any
|
|
237
208
|
# exit-code dispatch runs.
|
|
238
|
-
#
|
|
239
|
-
# @param argv [Array<String>]
|
|
240
|
-
# @param extra_env [Hash{String => String}]
|
|
241
|
-
# @yieldparam io [IO]
|
|
242
|
-
# @return [Yobi::IOHandle] if no block is given
|
|
243
|
-
# @return [Object] the block's own return value, otherwise
|
|
244
|
-
def run_dump(argv, extra_env: {})
|
|
209
|
+
def run_dump(argv, extra_env: {}) # :nodoc:
|
|
245
210
|
ensure_minimum_version!
|
|
246
211
|
output = Yobi::ResticOutput.new
|
|
247
212
|
read_end, write_end = IO.pipe
|
|
@@ -266,13 +231,11 @@ module Yobi
|
|
|
266
231
|
raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
|
|
267
232
|
end
|
|
268
233
|
|
|
269
|
-
# Verifies the installed Restic binary meets
|
|
270
|
-
#
|
|
234
|
+
# Verifies the installed Restic binary meets MINIMUM_VERSION, once per
|
|
235
|
+
# instance (memoized). #run/#run_dump/#run_mount call this
|
|
271
236
|
# automatically before executing a real command; public so a caller
|
|
272
|
-
# can also call it explicitly to fail fast.
|
|
273
|
-
#
|
|
274
|
-
# @return [void]
|
|
275
|
-
# @raise [Yobi::UnsupportedResticVersion]
|
|
237
|
+
# can also call it explicitly to fail fast. Raises
|
|
238
|
+
# Yobi::UnsupportedResticVersion when too old.
|
|
276
239
|
def ensure_minimum_version!
|
|
277
240
|
return if defined?(@version_checked)
|
|
278
241
|
@version_checked = true
|
|
@@ -288,7 +251,7 @@ module Yobi
|
|
|
288
251
|
# An old enough Restic ignores --json for `version` entirely and
|
|
289
252
|
# prints plain text instead (e.g. "restic 0.9.6 compiled with
|
|
290
253
|
# go1.13.4 on linux/amd64"), which JSON.parse can't handle.
|
|
291
|
-
VERSION_LINE_PATTERN = /restic\s+(\d+\.\d+\.\d+\S*)/
|
|
254
|
+
VERSION_LINE_PATTERN = /restic\s+(\d+\.\d+\.\d+\S*)/ # :nodoc:
|
|
292
255
|
|
|
293
256
|
def parse_version_output(raw)
|
|
294
257
|
JSON.parse(raw)
|
|
@@ -357,24 +320,24 @@ module Yobi
|
|
|
357
320
|
end
|
|
358
321
|
end
|
|
359
322
|
|
|
360
|
-
# The result of one
|
|
323
|
+
# The result of one Yobi::Restic#version call.
|
|
361
324
|
class ResticVersion < Yobi::FancyHash
|
|
362
|
-
#
|
|
325
|
+
# The Restic version string, e.g. +"0.19.1"+.
|
|
363
326
|
def version
|
|
364
327
|
self["version"]
|
|
365
328
|
end
|
|
366
329
|
|
|
367
|
-
#
|
|
330
|
+
# The Go compiler version Restic was built with, if reported.
|
|
368
331
|
def go_version
|
|
369
332
|
self["go_version"]
|
|
370
333
|
end
|
|
371
334
|
|
|
372
|
-
#
|
|
335
|
+
# The OS Restic was built for, if reported.
|
|
373
336
|
def go_os
|
|
374
337
|
self["go_os"]
|
|
375
338
|
end
|
|
376
339
|
|
|
377
|
-
#
|
|
340
|
+
# The CPU architecture Restic was built for, if reported.
|
|
378
341
|
def go_arch
|
|
379
342
|
self["go_arch"]
|
|
380
343
|
end
|
data/lib/yobi/restic_output.rb
CHANGED
|
@@ -4,21 +4,11 @@ require "json"
|
|
|
4
4
|
require "tempfile"
|
|
5
5
|
|
|
6
6
|
module Yobi
|
|
7
|
-
|
|
8
|
-
# written to, with random access so a caller asking only for the last
|
|
9
|
-
# line (see {#last_line}) doesn't have to read the entire output.
|
|
10
|
-
#
|
|
11
|
-
# @private
|
|
12
|
-
class ResticOutput
|
|
13
|
-
# Default read window size, in bytes, for {#last_line}.
|
|
7
|
+
class ResticOutput # :nodoc:
|
|
14
8
|
DEFAULT_TAIL_CHUNK_SIZE = 4096
|
|
15
9
|
|
|
16
|
-
# @return [File] a fresh, already-unlinked tempfile of its own
|
|
17
10
|
attr_reader :file
|
|
18
11
|
|
|
19
|
-
# @param tail_chunk_size [Integer] read window size for {#last_line}
|
|
20
|
-
# @param transform [Proc, nil] used by {#write_and_parse} during a streaming run to transform
|
|
21
|
-
# each message, and as {#messages}' default transform afterward, when no explicit one is given
|
|
22
12
|
def initialize(tail_chunk_size: DEFAULT_TAIL_CHUNK_SIZE, transform: nil)
|
|
23
13
|
@file = Tempfile.new("yobi-restic-output")
|
|
24
14
|
@file.unlink
|
|
@@ -28,25 +18,15 @@ module Yobi
|
|
|
28
18
|
@stderr_offsets = []
|
|
29
19
|
end
|
|
30
20
|
|
|
31
|
-
# Byte offsets of every line, grouped by message_type. Built once and
|
|
32
|
-
# memoized.
|
|
33
|
-
#
|
|
34
|
-
# @return [Hash{String => Array<Integer>}]
|
|
35
21
|
def index
|
|
36
22
|
@index ||= build_index
|
|
37
23
|
end
|
|
38
24
|
|
|
39
|
-
# @param offset [Integer] a byte offset, as recorded by {#index}
|
|
40
|
-
# @return [String] the line starting at that offset
|
|
41
25
|
def read_line_at(offset)
|
|
42
26
|
@file.seek(offset)
|
|
43
27
|
@file.gets
|
|
44
28
|
end
|
|
45
29
|
|
|
46
|
-
# The last non-blank line, found by seeking backward from the end of
|
|
47
|
-
# the file.
|
|
48
|
-
#
|
|
49
|
-
# @return [String, nil]
|
|
50
30
|
def last_line
|
|
51
31
|
size = @file.size
|
|
52
32
|
return nil if size.zero?
|
|
@@ -66,8 +46,6 @@ module Yobi
|
|
|
66
46
|
end
|
|
67
47
|
end
|
|
68
48
|
|
|
69
|
-
# @yieldparam line [String]
|
|
70
|
-
# @return [Enumerator] if no block is given
|
|
71
49
|
def each_line(&block)
|
|
72
50
|
return enum_for(:each_line) unless block_given?
|
|
73
51
|
|
|
@@ -75,17 +53,6 @@ module Yobi
|
|
|
75
53
|
@file.each_line(&block)
|
|
76
54
|
end
|
|
77
55
|
|
|
78
|
-
# Every message recorded in {#index}, restricted to one +message_type+
|
|
79
|
-
# if given, in file order. Without an explicit transform block, uses
|
|
80
|
-
# the +transform:+ given to {#initialize} (if any); with neither, just
|
|
81
|
-
# yields/returns the raw parsed Hash. Whenever a transform (explicit
|
|
82
|
-
# or the stored one) applies, wraps the result in a {LazyList}
|
|
83
|
-
# instead of yielding.
|
|
84
|
-
#
|
|
85
|
-
# @param message_type [String, nil] every message, if omitted
|
|
86
|
-
# @yieldparam message [Hash] the raw parsed message, to transform
|
|
87
|
-
# @return [Yobi::ResticOutput::LazyList] if a transform block (or a stored transform) applies
|
|
88
|
-
# @return [Enumerator] of raw Hashes, otherwise
|
|
89
56
|
def messages(message_type = nil, &transform)
|
|
90
57
|
transform ||= @transform
|
|
91
58
|
return raw_messages(message_type) unless transform
|
|
@@ -94,27 +61,12 @@ module Yobi
|
|
|
94
61
|
LazyList.new(self, offsets, transform)
|
|
95
62
|
end
|
|
96
63
|
|
|
97
|
-
# Every line known to have arrived on stderr rather than stdout during
|
|
98
|
-
# a streaming run (see {#write_and_parse}), in file order.
|
|
99
|
-
#
|
|
100
|
-
# @yieldparam line [String]
|
|
101
|
-
# @return [Enumerator] if no block is given
|
|
102
64
|
def stderr_lines
|
|
103
65
|
return enum_for(:stderr_lines) unless block_given?
|
|
104
66
|
|
|
105
67
|
@stderr_offsets.each { |offset| yield read_line_at(offset) }
|
|
106
68
|
end
|
|
107
69
|
|
|
108
|
-
# Called once per line read from a live streaming run's stdout/stderr.
|
|
109
|
-
# Writes it to the file, parses it, and - if it's a JSON message -
|
|
110
|
-
# indexes it by message_type and returns it transformed via the
|
|
111
|
-
# +transform:+ given to {#initialize} (or the raw parsed Hash, if none
|
|
112
|
-
# was given). A non-JSON line's offset is recorded in {#stderr_lines}
|
|
113
|
-
# instead, if +origin+ is +:stderr+; either way, returns nil.
|
|
114
|
-
#
|
|
115
|
-
# @param line [String]
|
|
116
|
-
# @param origin [:stdout, :stderr]
|
|
117
|
-
# @return [Object, nil]
|
|
118
70
|
def write_and_parse(line, origin)
|
|
119
71
|
offset = @file.pos
|
|
120
72
|
@file.write(line)
|
|
@@ -134,13 +86,11 @@ module Yobi
|
|
|
134
86
|
end
|
|
135
87
|
end
|
|
136
88
|
|
|
137
|
-
# @return [String] the full captured output
|
|
138
89
|
def to_s
|
|
139
90
|
@file.rewind
|
|
140
91
|
@file.read
|
|
141
92
|
end
|
|
142
93
|
|
|
143
|
-
# @return [String]
|
|
144
94
|
def inspect
|
|
145
95
|
"#<#{self.class} #{@file.size} bytes>"
|
|
146
96
|
end
|
|
@@ -179,32 +129,17 @@ module Yobi
|
|
|
179
129
|
index
|
|
180
130
|
end
|
|
181
131
|
|
|
182
|
-
|
|
183
|
-
# like a plain Array (bounded to a preview, unlike one). Returned by
|
|
184
|
-
# {ResticOutput#messages} when given a transform block.
|
|
185
|
-
# #inspect/#pretty_print's preview is memoized, and #each reuses it
|
|
186
|
-
# rather than re-reading/re-transforming those same offsets again
|
|
187
|
-
# when the caller iterates the whole thing.
|
|
188
|
-
#
|
|
189
|
-
# @private
|
|
190
|
-
class LazyList
|
|
132
|
+
class LazyList # :nodoc:
|
|
191
133
|
include Enumerable
|
|
192
134
|
|
|
193
|
-
# Items #inspect/#pretty_print show before truncating with "...".
|
|
194
|
-
# One louder than ActiveRecord::Relation#inspect's own preview size.
|
|
195
135
|
INSPECT_PREVIEW_SIZE = 11
|
|
196
136
|
|
|
197
|
-
# @param output [Yobi::ResticOutput]
|
|
198
|
-
# @param offsets [Array<Integer>]
|
|
199
|
-
# @param transform [Proc] applied to each offset's raw parsed Hash
|
|
200
137
|
def initialize(output, offsets, transform)
|
|
201
138
|
@output = output
|
|
202
139
|
@offsets = offsets
|
|
203
140
|
@transform = transform
|
|
204
141
|
end
|
|
205
142
|
|
|
206
|
-
# @yieldparam item [Object]
|
|
207
|
-
# @return [Enumerator] if no block is given
|
|
208
143
|
def each
|
|
209
144
|
return enum_for(:each) unless block_given?
|
|
210
145
|
|
|
@@ -212,23 +147,17 @@ module Yobi
|
|
|
212
147
|
@offsets[preview.size..].each { |offset| yield transform_at(offset) }
|
|
213
148
|
end
|
|
214
149
|
|
|
215
|
-
# The number of messages, known upfront from {#index} - unlike
|
|
216
|
-
# +Enumerable#count+, doesn't read/transform a single one to answer.
|
|
217
|
-
#
|
|
218
|
-
# @return [Integer]
|
|
219
150
|
def size
|
|
220
151
|
@offsets.size
|
|
221
152
|
end
|
|
222
153
|
alias_method :length, :size
|
|
223
154
|
|
|
224
|
-
# @return [String]
|
|
225
155
|
def inspect
|
|
226
156
|
shown = preview.first(INSPECT_PREVIEW_SIZE).map(&:inspect).join(", ")
|
|
227
157
|
shown += ", ..." if preview.size > INSPECT_PREVIEW_SIZE
|
|
228
158
|
"[#{shown}]"
|
|
229
159
|
end
|
|
230
160
|
|
|
231
|
-
# @return [void]
|
|
232
161
|
def pretty_print(q)
|
|
233
162
|
q.group(1, "[", "]") do
|
|
234
163
|
q.seplist(preview.first(INSPECT_PREVIEW_SIZE)) { |item| q.pp item }
|
|
@@ -238,7 +167,6 @@ module Yobi
|
|
|
238
167
|
|
|
239
168
|
private
|
|
240
169
|
|
|
241
|
-
# @return [Array<Object>]
|
|
242
170
|
def preview
|
|
243
171
|
@preview ||= @offsets.first(INSPECT_PREVIEW_SIZE + 1).map { |offset| transform_at(offset) }
|
|
244
172
|
end
|
data/lib/yobi/snapshot.rb
CHANGED
|
@@ -3,120 +3,108 @@
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
5
|
module Yobi
|
|
6
|
-
# One snapshot, as Restic reports it. Shared across
|
|
7
|
-
#
|
|
6
|
+
# One snapshot, as Restic reports it. Shared across Yobi::Repository#snapshots,
|
|
7
|
+
# Yobi::Repository#ls, and Yobi::Repository#forget's keep/remove entries.
|
|
8
8
|
class Snapshot < Yobi::FancyHash
|
|
9
|
-
#
|
|
9
|
+
# Full snapshot ID (64-char hex).
|
|
10
10
|
def id
|
|
11
11
|
self["id"]
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
#
|
|
14
|
+
# First 8 chars of #id.
|
|
15
15
|
def short_id
|
|
16
16
|
self["short_id"]
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
#
|
|
19
|
+
# When the snapshot was created.
|
|
20
20
|
def time
|
|
21
21
|
@time ||= Time.parse(self["time"])
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
#
|
|
24
|
+
# Hostname recorded on the snapshot.
|
|
25
25
|
def host
|
|
26
26
|
self["hostname"]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
#
|
|
29
|
+
# Tags recorded on the snapshot.
|
|
30
30
|
def tags
|
|
31
31
|
@tags ||= self["tags"] || []
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# Paths backed up in the snapshot.
|
|
35
35
|
def paths
|
|
36
36
|
self["paths"]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
#
|
|
39
|
+
# Parent snapshot's #id, if any.
|
|
40
40
|
def parent_id
|
|
41
41
|
self["parent"]
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
#
|
|
44
|
+
# A Yobi::SnapshotSummary of the snapshot's stats at creation time.
|
|
45
45
|
def summary
|
|
46
46
|
@summary ||= SnapshotSummary.new(self["summary"] || {})
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
# The
|
|
51
|
-
# was created.
|
|
52
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#snapshotsummary-object
|
|
50
|
+
# The +"summary"+ field of a Yobi::Snapshot, the stats recorded when it
|
|
51
|
+
# was created. See
|
|
52
|
+
# https://restic.readthedocs.io/en/stable/075_scripting.html#snapshotsummary-object.
|
|
53
53
|
class SnapshotSummary < Yobi::FancyHash
|
|
54
|
-
#
|
|
54
|
+
# When the backup started.
|
|
55
55
|
def backup_start
|
|
56
56
|
@backup_start ||= Time.parse(self["backup_start"]) if self["backup_start"]
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
#
|
|
59
|
+
# When the backup finished.
|
|
60
60
|
def backup_end
|
|
61
61
|
@backup_end ||= Time.parse(self["backup_end"]) if self["backup_end"]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
# @return [Integer]
|
|
65
64
|
def files_new
|
|
66
65
|
self["files_new"] || 0
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
# @return [Integer]
|
|
70
68
|
def files_changed
|
|
71
69
|
self["files_changed"] || 0
|
|
72
70
|
end
|
|
73
71
|
|
|
74
|
-
# @return [Integer]
|
|
75
72
|
def files_unmodified
|
|
76
73
|
self["files_unmodified"] || 0
|
|
77
74
|
end
|
|
78
75
|
|
|
79
|
-
# @return [Integer]
|
|
80
76
|
def dirs_new
|
|
81
77
|
self["dirs_new"] || 0
|
|
82
78
|
end
|
|
83
79
|
|
|
84
|
-
# @return [Integer]
|
|
85
80
|
def dirs_changed
|
|
86
81
|
self["dirs_changed"] || 0
|
|
87
82
|
end
|
|
88
83
|
|
|
89
|
-
# @return [Integer]
|
|
90
84
|
def dirs_unmodified
|
|
91
85
|
self["dirs_unmodified"] || 0
|
|
92
86
|
end
|
|
93
87
|
|
|
94
|
-
# @return [Integer]
|
|
95
88
|
def data_blobs
|
|
96
89
|
self["data_blobs"] || 0
|
|
97
90
|
end
|
|
98
91
|
|
|
99
|
-
# @return [Integer]
|
|
100
92
|
def tree_blobs
|
|
101
93
|
self["tree_blobs"] || 0
|
|
102
94
|
end
|
|
103
95
|
|
|
104
|
-
# @return [Integer]
|
|
105
96
|
def data_added
|
|
106
97
|
self["data_added"] || 0
|
|
107
98
|
end
|
|
108
99
|
|
|
109
|
-
# @return [Integer]
|
|
110
100
|
def data_added_packed
|
|
111
101
|
self["data_added_packed"] || 0
|
|
112
102
|
end
|
|
113
103
|
|
|
114
|
-
# @return [Integer]
|
|
115
104
|
def total_files_processed
|
|
116
105
|
self["total_files_processed"] || 0
|
|
117
106
|
end
|
|
118
107
|
|
|
119
|
-
# @return [Integer]
|
|
120
108
|
def total_bytes_processed
|
|
121
109
|
self["total_bytes_processed"] || 0
|
|
122
110
|
end
|
data/lib/yobi/version.rb
CHANGED
data/sig/yobi.rbs
CHANGED
|
@@ -227,7 +227,7 @@ module Yobi
|
|
|
227
227
|
MINIMUM_VERSION: Gem::Version
|
|
228
228
|
ALLOWED_ENV_VARS: Set[String]
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
attr_accessor restic_path: String
|
|
231
231
|
attr_accessor cache_dir: String?
|
|
232
232
|
attr_accessor compression: String?
|
|
233
233
|
attr_accessor pack_size: Integer?
|
|
@@ -287,9 +287,9 @@ module Yobi
|
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
class Repository
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
attr_accessor url: String
|
|
291
|
+
attr_accessor password: password
|
|
292
|
+
attr_accessor backend_credentials: backend_credentials
|
|
293
293
|
|
|
294
294
|
def initialize: (url: String, password: password, ?backend_credentials: backend_credentials, ?restic: restic_or_path) -> void
|
|
295
295
|
|