yobi 0.3.1 → 1.1.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 +83 -44
- data/lib/yobi/argv_builder.rb +1 -35
- data/lib/yobi/cancellable_proxy.rb +48 -0
- data/lib/yobi/cancellation.rb +97 -0
- data/lib/yobi/errors.rb +45 -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 +23 -43
- 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 +17 -37
- data/lib/yobi/repository/forget.rb +35 -39
- data/lib/yobi/repository/init.rb +19 -24
- data/lib/yobi/repository/key.rb +18 -33
- 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 +7 -8
- data/lib/yobi/repository/stats.rb +26 -21
- data/lib/yobi/repository/tag.rb +23 -33
- data/lib/yobi/repository/unlock.rb +2 -4
- data/lib/yobi/repository.rb +96 -25
- data/lib/yobi/restic.rb +93 -114
- data/lib/yobi/restic_output.rb +2 -74
- data/lib/yobi/snapshot.rb +15 -27
- data/lib/yobi/version.rb +1 -2
- data/lib/yobi.rb +2 -0
- data/sig/yobi.rbs +83 -6
- metadata +5 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
# A cancellation token for a long-running Restic run.
|
|
5
|
+
#
|
|
6
|
+
# Pass one to Yobi::Repository#with_cancellation, then call #cancel! from
|
|
7
|
+
# another thread to stop whichever long-running method (#backup,
|
|
8
|
+
# #restore, #check, #prune, #forget, #copy) is running on the repository
|
|
9
|
+
# it returns:
|
|
10
|
+
#
|
|
11
|
+
# token = Yobi::Cancellation.new
|
|
12
|
+
# Thread.new { token.cancel! if user_clicked_cancel }
|
|
13
|
+
# repo.with_cancellation(token).backup(source: "/data")
|
|
14
|
+
# # => raises Yobi::Cancelled
|
|
15
|
+
#
|
|
16
|
+
# #cancel! sends the Restic process SIGINT rather than SIGKILL, so Restic
|
|
17
|
+
# removes its own repository lock on the way out and no +restic unlock+ is
|
|
18
|
+
# needed afterwards. An interrupted +backup+ writes no snapshot; data it had
|
|
19
|
+
# already uploaded is generally left unreferenced until the next +prune+
|
|
20
|
+
# rather than being reused, so cancelling does discard in-progress work.
|
|
21
|
+
#
|
|
22
|
+
# Safe to call #cancel! before the run starts (the run then raises without
|
|
23
|
+
# spawning Restic at all), after it has finished (a no-op), and from any
|
|
24
|
+
# thread. A token tracks one run at a time and is not reusable once
|
|
25
|
+
# cancelled.
|
|
26
|
+
class Cancellation
|
|
27
|
+
# The signal sent to Restic by #cancel! unless overridden.
|
|
28
|
+
DEFAULT_SIGNAL = "INT"
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@cancelled = false
|
|
33
|
+
@signal = DEFAULT_SIGNAL
|
|
34
|
+
@pid = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# True once #cancel! has been called.
|
|
38
|
+
def cancelled?
|
|
39
|
+
@mutex.synchronize { @cancelled }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Requests cancellation, signalling the attached Restic process if one is
|
|
43
|
+
# currently running. Returns true the first time, false if this token was
|
|
44
|
+
# already cancelled. Idempotent and thread-safe.
|
|
45
|
+
#
|
|
46
|
+
# +signal:+ overrides the signal sent; the SIGINT default is what lets
|
|
47
|
+
# Restic clean up its lock, so override it only when you have a reason to.
|
|
48
|
+
def cancel!(signal: DEFAULT_SIGNAL)
|
|
49
|
+
pid = nil
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
return false if @cancelled
|
|
52
|
+
|
|
53
|
+
@cancelled = true
|
|
54
|
+
@signal = signal
|
|
55
|
+
pid = @pid
|
|
56
|
+
end
|
|
57
|
+
signal_pid(pid, signal)
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Attaches a freshly spawned Restic process. If #cancel! already landed
|
|
62
|
+
# before the spawn, the process is signalled immediately. Called by
|
|
63
|
+
# Yobi::Restic; not part of the public API.
|
|
64
|
+
def attach(pid) # :nodoc:
|
|
65
|
+
cancelled, signal = @mutex.synchronize do
|
|
66
|
+
@pid = pid
|
|
67
|
+
[@cancelled, @signal]
|
|
68
|
+
end
|
|
69
|
+
signal_pid(pid, signal) if cancelled
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Detaches the current process once it has been reaped, so a later
|
|
73
|
+
# #cancel! can't signal a recycled PID. Called by Yobi::Restic; not part
|
|
74
|
+
# of the public API.
|
|
75
|
+
def detach # :nodoc:
|
|
76
|
+
@mutex.synchronize { @pid = nil }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect
|
|
80
|
+
"#<#{self.class} cancelled=#{cancelled?}>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Errno::ESRCH means Restic already exited between the cancel and the
|
|
86
|
+
# signal, which is exactly the outcome cancelling wanted.
|
|
87
|
+
def signal_pid(pid, signal)
|
|
88
|
+
return if pid.nil?
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
Process.kill(signal, pid)
|
|
92
|
+
rescue Errno::ESRCH
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/yobi/errors.rb
CHANGED
|
@@ -6,53 +6,73 @@ module Yobi
|
|
|
6
6
|
# Base class for all errors raised by Yobi.
|
|
7
7
|
class Error < StandardError; end
|
|
8
8
|
|
|
9
|
-
# The
|
|
10
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#exit-errors
|
|
9
|
+
# The +exit_error+ message from a fatal Restic run, if one was printed.
|
|
10
|
+
# See https://restic.readthedocs.io/en/stable/075_scripting.html#exit-errors.
|
|
11
11
|
class ExitError < Yobi::FancyHash
|
|
12
|
-
#
|
|
13
|
-
LINE_PATTERN = /"message_type"\s*:\s*"exit_error"/
|
|
12
|
+
LINE_PATTERN = /"message_type"\s*:\s*"exit_error"/ # :nodoc:
|
|
14
13
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Builds an ExitError from a Yobi::ResticOutput, or +nil+ if no
|
|
15
|
+
# +exit_error+ line is present in it.
|
|
17
16
|
def self.from_output(output)
|
|
18
17
|
line = output.each_line.find { |candidate| LINE_PATTERN.match?(candidate) }
|
|
19
18
|
new(JSON.parse(line)) if line
|
|
20
19
|
end
|
|
21
20
|
|
|
22
|
-
#
|
|
21
|
+
# Restic's own +exit_error+ code String, if present.
|
|
23
22
|
def code
|
|
24
23
|
self["code"]
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
#
|
|
26
|
+
# Restic's own +exit_error+ message String, if present.
|
|
28
27
|
def message
|
|
29
28
|
self["message"]
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
# Raised when the installed Restic binary is older than
|
|
34
|
-
#
|
|
33
|
+
# Yobi::Restic::MINIMUM_VERSION.
|
|
35
34
|
class UnsupportedResticVersion < Error
|
|
36
|
-
# @return [String]
|
|
37
35
|
attr_reader :installed_version, :minimum_version
|
|
38
36
|
|
|
39
|
-
#
|
|
40
|
-
def initialize(installed_version:, minimum_version:)
|
|
37
|
+
def initialize(installed_version:, minimum_version:) # :nodoc:
|
|
41
38
|
@installed_version = installed_version
|
|
42
39
|
@minimum_version = minimum_version
|
|
43
40
|
super("Restic #{installed_version} does not meet the minimum supported version #{minimum_version}")
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
|
|
44
|
+
# Raised when a run was stopped through a Yobi::Cancellation token.
|
|
45
|
+
#
|
|
46
|
+
# Distinct from Yobi::ResticExecutionError and its subclasses: the run did
|
|
47
|
+
# not fail, the caller stopped it on purpose. Rescue this to record a
|
|
48
|
+
# cancellation without treating it as a backup failure.
|
|
49
|
+
class Cancelled < Error
|
|
50
|
+
# The raw +{exit_code:, output:, argv:}+ result. +exit_code:+ and
|
|
51
|
+
# +output:+ are +nil+ when the token was already cancelled before
|
|
52
|
+
# Restic was spawned at all.
|
|
53
|
+
attr_reader :execution
|
|
54
|
+
|
|
55
|
+
def initialize(execution) # :nodoc:
|
|
56
|
+
@execution = execution
|
|
57
|
+
super("Restic run was cancelled")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The argv of the run that was cancelled.
|
|
61
|
+
def argv
|
|
62
|
+
execution[:argv]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# True when the token was cancelled before Restic even started.
|
|
66
|
+
def before_start?
|
|
67
|
+
execution[:exit_code].nil?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
47
71
|
# Raised when the Restic binary itself can't be found or executed.
|
|
48
72
|
class ResticNotFound < Error
|
|
49
|
-
|
|
50
|
-
attr_reader :restic_path
|
|
51
|
-
# @return [Array<String>]
|
|
52
|
-
attr_reader :argv
|
|
73
|
+
attr_reader :restic_path, :argv
|
|
53
74
|
|
|
54
|
-
#
|
|
55
|
-
def initialize(restic_path:, argv:)
|
|
75
|
+
def initialize(restic_path:, argv:) # :nodoc:
|
|
56
76
|
@restic_path = restic_path
|
|
57
77
|
@argv = argv
|
|
58
78
|
super("Restic binary not found: #{restic_path.inspect} (tried to run #{argv.inspect})")
|
|
@@ -62,13 +82,12 @@ module Yobi
|
|
|
62
82
|
# Raised when Restic exits with a failure code. Base class for the
|
|
63
83
|
# specific typed errors below.
|
|
64
84
|
class ResticExecutionError < Error
|
|
65
|
-
#
|
|
85
|
+
# The raw +{exit_code:, output:, argv:}+ execution result.
|
|
66
86
|
attr_reader :execution
|
|
67
|
-
#
|
|
87
|
+
# A Yobi::ExitError parsed out of the output, if one is present.
|
|
68
88
|
attr_reader :exit_error
|
|
69
89
|
|
|
70
|
-
#
|
|
71
|
-
def initialize(execution)
|
|
90
|
+
def initialize(execution) # :nodoc:
|
|
72
91
|
@execution = execution
|
|
73
92
|
@exit_error = Yobi::ExitError.from_output(execution[:output])
|
|
74
93
|
super(exit_error&.message || "Restic exited with status #{execution[:exit_code]}: #{execution[:output]}")
|
|
@@ -87,16 +106,12 @@ module Yobi
|
|
|
87
106
|
# Raised when Restic exits with a failure code not otherwise classified above.
|
|
88
107
|
class ResticCommandFailed < ResticExecutionError; end
|
|
89
108
|
|
|
90
|
-
# Raised by
|
|
91
|
-
# ready within
|
|
109
|
+
# Raised by Yobi::Repository#mount when Restic doesn't report itself
|
|
110
|
+
# ready within +ready_timeout:+ seconds.
|
|
92
111
|
class MountTimeout < Error
|
|
93
|
-
|
|
94
|
-
attr_reader :argv
|
|
95
|
-
# @return [Numeric]
|
|
96
|
-
attr_reader :timeout
|
|
112
|
+
attr_reader :argv, :timeout
|
|
97
113
|
|
|
98
|
-
#
|
|
99
|
-
def initialize(argv:, timeout:)
|
|
114
|
+
def initialize(argv:, timeout:) # :nodoc:
|
|
100
115
|
@argv = argv
|
|
101
116
|
@timeout = timeout
|
|
102
117
|
super("Restic mount didn't report readiness within #{timeout}s (tried to run #{argv.inspect})")
|
data/lib/yobi/fancy_hash.rb
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
|
-
|
|
5
|
-
class FancyHash < Hash
|
|
6
|
-
# @private
|
|
4
|
+
class FancyHash < Hash # :nodoc:
|
|
7
5
|
def initialize(raw)
|
|
8
6
|
super()
|
|
9
7
|
replace(raw)
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
# @return [String]
|
|
13
10
|
def inspect
|
|
14
11
|
"#<#{self.class} #{super}>"
|
|
15
12
|
end
|
|
16
13
|
|
|
17
|
-
# @return [void]
|
|
18
14
|
def pretty_print(q)
|
|
19
15
|
q.object_group(self) do
|
|
20
16
|
each do |key, value|
|
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
|