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/repository/key.rb
CHANGED
|
@@ -6,19 +6,17 @@ require "tempfile"
|
|
|
6
6
|
|
|
7
7
|
module Yobi
|
|
8
8
|
class Repository
|
|
9
|
-
#
|
|
9
|
+
# +restic key add+: creates a new key (password) for this repository.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# `password:`
|
|
21
|
-
# @return [true]
|
|
11
|
+
# +new_password:+ accepts the same shapes as #initialize's +password:+
|
|
12
|
+
# minus +[:command, ...]+: a literal String; a +[:file, "..."]+ tuple,
|
|
13
|
+
# resolved natively by Restic itself; +:insecure_no_password+; or
|
|
14
|
+
# anything responding to +#call+. A literal String or callable is
|
|
15
|
+
# written to a briefly-lived, 0600-permissioned tempfile, since Restic
|
|
16
|
+
# itself only accepts a new password by file or interactive prompt.
|
|
17
|
+
#
|
|
18
|
+
# +host:+ and +user:+ optionally override the hostname/username
|
|
19
|
+
# recorded on the new key.
|
|
22
20
|
def key_add(new_password:, host: nil, user: nil)
|
|
23
21
|
with_new_password_flag(new_password) do |flag_name, flag_value|
|
|
24
22
|
argv = build_argv("key", "add") do |a|
|
|
@@ -32,24 +30,19 @@ module Yobi
|
|
|
32
30
|
end
|
|
33
31
|
alias_method :add_key, :key_add
|
|
34
32
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# @return [Array<Yobi::Key>]
|
|
33
|
+
# +restic key list+: every Yobi::Key associated with this repository.
|
|
38
34
|
def key_list
|
|
39
35
|
execution = run_restic(build_argv("key", "list"))
|
|
40
|
-
|
|
36
|
+
parse_json_output(execution).map { |raw| Key.new(raw) }
|
|
41
37
|
end
|
|
42
38
|
alias_method :keys, :key_list
|
|
43
39
|
|
|
44
|
-
#
|
|
40
|
+
# +restic key passwd+: creates a new key for this repository and
|
|
45
41
|
# removes the key currently in use. On success, also updates this
|
|
46
|
-
#
|
|
42
|
+
# Repository instance's own +password:+ to +new_password:+, so it
|
|
47
43
|
# keeps working against this same repository afterward.
|
|
48
44
|
#
|
|
49
|
-
#
|
|
50
|
-
# @param user [String, nil] username to record on the new key
|
|
51
|
-
# @param new_password [String, Array, Symbol, #call] see {#key_add}
|
|
52
|
-
# @return [true]
|
|
45
|
+
# +new_password:+, +host:+ and +user:+ take the same shapes as #key_add.
|
|
53
46
|
def key_passwd(new_password:, host: nil, user: nil)
|
|
54
47
|
with_new_password_flag(new_password) do |flag_name, flag_value|
|
|
55
48
|
argv = build_argv("key", "passwd") do |a|
|
|
@@ -64,12 +57,8 @@ module Yobi
|
|
|
64
57
|
end
|
|
65
58
|
alias_method :change_password, :key_passwd
|
|
66
59
|
|
|
67
|
-
#
|
|
68
|
-
# Restic refuses to remove the key currently being used to access
|
|
69
|
-
# repository.
|
|
70
|
-
#
|
|
71
|
-
# @param id [String]
|
|
72
|
-
# @return [true]
|
|
60
|
+
# +restic key remove+: removes the given key +id+ from this repository.
|
|
61
|
+
# Restic refuses to remove the key currently being used to access it.
|
|
73
62
|
def key_remove(id:)
|
|
74
63
|
run_restic(build_argv("key", "remove", id))
|
|
75
64
|
true
|
|
@@ -105,29 +94,25 @@ module Yobi
|
|
|
105
94
|
end
|
|
106
95
|
end
|
|
107
96
|
|
|
108
|
-
# One key (password) associated with the repository, from
|
|
97
|
+
# One key (password) associated with the repository, from Yobi::Repository#key_list.
|
|
109
98
|
class Key < Yobi::FancyHash
|
|
110
|
-
# @return [String]
|
|
111
99
|
def id
|
|
112
100
|
self["id"]
|
|
113
101
|
end
|
|
114
102
|
|
|
115
|
-
# @return [String]
|
|
116
103
|
def user_name
|
|
117
104
|
self["userName"]
|
|
118
105
|
end
|
|
119
106
|
|
|
120
|
-
# @return [String]
|
|
121
107
|
def host_name
|
|
122
108
|
self["hostName"]
|
|
123
109
|
end
|
|
124
110
|
|
|
125
|
-
#
|
|
111
|
+
# Whether this is the key currently in use.
|
|
126
112
|
def current?
|
|
127
113
|
self["current"]
|
|
128
114
|
end
|
|
129
115
|
|
|
130
|
-
# @return [Time]
|
|
131
116
|
def created
|
|
132
117
|
@created ||= Time.parse(self["created"])
|
|
133
118
|
end
|
data/lib/yobi/repository/list.rb
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# @return [Array<String>]
|
|
5
|
+
# +restic list+: every object ID of the given +type+ in this repository.
|
|
6
|
+
# +type+ is +:blobs+, +:packs+, +:index+, +:snapshots+, +:keys+, or
|
|
7
|
+
# +:locks+. Restic ignores +--json+ for this command; each line is a
|
|
8
|
+
# bare ID, except for +:blobs+, where each line is +"data <id>"+ or
|
|
9
|
+
# +"tree <id>"+.
|
|
11
10
|
def list(type)
|
|
12
11
|
argv = build_argv("list", type)
|
|
13
12
|
execution = run_restic(argv)
|
data/lib/yobi/repository/ls.rb
CHANGED
|
@@ -4,19 +4,18 @@ require "time"
|
|
|
4
4
|
|
|
5
5
|
module Yobi
|
|
6
6
|
class Repository
|
|
7
|
-
#
|
|
7
|
+
# +restic ls+: lists a snapshot's files/directories.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# @return [Yobi::LsOutcome]
|
|
9
|
+
# +snapshot_id:+ is a snapshot ID, or +"latest"+. +dirs:+ restricts the
|
|
10
|
+
# listing to specific directories within the snapshot.
|
|
11
|
+
#
|
|
12
|
+
# +hosts:+, +paths:+ and +tags:+ each accept a single value or an Array
|
|
13
|
+
# and are only relevant when +snapshot_id+ is +"latest"+.
|
|
14
|
+
#
|
|
15
|
+
# +recursive:+ recurses into subdirectories. +human_readable:+, +long:+,
|
|
16
|
+
# +reverse:+ and +sort:+ (e.g. +"size"+) toggle formatting/sort options.
|
|
17
|
+
#
|
|
18
|
+
# Returns a Yobi::LsOutcome.
|
|
20
19
|
def ls(snapshot_id:, dirs: [], hosts: [], human_readable: false, long: false, paths: [],
|
|
21
20
|
recursive: false, reverse: false, sort: nil, tags: [])
|
|
22
21
|
argv = build_argv("ls", snapshot_id) do |a|
|
|
@@ -36,16 +35,7 @@ module Yobi
|
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
# in the matching typed class. Used both as the {Yobi::ResticOutput}
|
|
41
|
-
# `transform:` for {Yobi::Repository#ls} and by {Yobi::LsOutcome}'s own
|
|
42
|
-
# post-hoc accessors.
|
|
43
|
-
#
|
|
44
|
-
# @private
|
|
45
|
-
module LsMessageWrapper
|
|
46
|
-
# @param raw [Hash]
|
|
47
|
-
# @return [Yobi::Snapshot, Yobi::Node, Hash]
|
|
48
|
-
# the raw Hash itself for a message_type this version of Yobi doesn't recognize
|
|
38
|
+
module LsMessageWrapper # :nodoc:
|
|
49
39
|
def self.call(raw)
|
|
50
40
|
case raw["message_type"]
|
|
51
41
|
when "snapshot" then Snapshot.new(raw)
|
|
@@ -55,28 +45,26 @@ module Yobi
|
|
|
55
45
|
end
|
|
56
46
|
end
|
|
57
47
|
|
|
58
|
-
# The outcome of one
|
|
48
|
+
# The outcome of one Yobi::Repository#ls call.
|
|
59
49
|
class LsOutcome
|
|
60
|
-
#
|
|
50
|
+
# The Yobi::ResticOutput backing this outcome.
|
|
61
51
|
attr_reader :output
|
|
62
52
|
|
|
63
|
-
#
|
|
64
|
-
def initialize(execution)
|
|
53
|
+
def initialize(execution) # :nodoc:
|
|
65
54
|
@output = execution[:output]
|
|
66
55
|
end
|
|
67
56
|
|
|
68
|
-
#
|
|
57
|
+
# The resolved Yobi::Snapshot, useful when +snapshot_id:+ was +"latest"+.
|
|
69
58
|
def snapshot
|
|
70
59
|
@snapshot ||= output.messages("snapshot").first
|
|
71
60
|
end
|
|
72
61
|
|
|
73
|
-
#
|
|
62
|
+
# Every Yobi::Node from the run.
|
|
74
63
|
def nodes
|
|
75
64
|
@nodes ||= output.messages("node")
|
|
76
65
|
end
|
|
77
66
|
alias_method :entries, :nodes
|
|
78
67
|
|
|
79
|
-
# @return [void]
|
|
80
68
|
def pretty_print(q)
|
|
81
69
|
q.object_group(self) do
|
|
82
70
|
q.breakable
|
|
@@ -87,61 +75,50 @@ module Yobi
|
|
|
87
75
|
end
|
|
88
76
|
end
|
|
89
77
|
|
|
90
|
-
# One file/directory entry from a
|
|
91
|
-
#
|
|
92
|
-
#
|
|
78
|
+
# One file/directory entry from a Yobi::Repository#ls call. Restic's own
|
|
79
|
+
# term for this is a "node" - the same structure it uses internally for
|
|
80
|
+
# every entry in a snapshot's tree, not just ones +ls+ happens to show.
|
|
93
81
|
class Node < Yobi::FancyHash
|
|
94
|
-
# @return [String]
|
|
95
82
|
def name
|
|
96
83
|
self["name"]
|
|
97
84
|
end
|
|
98
85
|
|
|
99
|
-
# @return [String]
|
|
100
86
|
def type
|
|
101
87
|
self["type"]
|
|
102
88
|
end
|
|
103
89
|
|
|
104
|
-
# @return [String]
|
|
105
90
|
def path
|
|
106
91
|
self["path"]
|
|
107
92
|
end
|
|
108
93
|
|
|
109
|
-
# @return [Integer]
|
|
110
94
|
def size
|
|
111
95
|
self["size"]
|
|
112
96
|
end
|
|
113
97
|
|
|
114
|
-
# @return [String]
|
|
115
98
|
def permissions
|
|
116
99
|
self["permissions"]
|
|
117
100
|
end
|
|
118
101
|
|
|
119
|
-
# @return [Integer]
|
|
120
102
|
def uid
|
|
121
103
|
self["uid"]
|
|
122
104
|
end
|
|
123
105
|
|
|
124
|
-
# @return [Integer]
|
|
125
106
|
def gid
|
|
126
107
|
self["gid"]
|
|
127
108
|
end
|
|
128
109
|
|
|
129
|
-
# @return [Integer]
|
|
130
110
|
def inode
|
|
131
111
|
self["inode"]
|
|
132
112
|
end
|
|
133
113
|
|
|
134
|
-
# @return [Time]
|
|
135
114
|
def mtime
|
|
136
115
|
@mtime ||= Time.parse(self["mtime"])
|
|
137
116
|
end
|
|
138
117
|
|
|
139
|
-
# @return [Time]
|
|
140
118
|
def atime
|
|
141
119
|
@atime ||= Time.parse(self["atime"])
|
|
142
120
|
end
|
|
143
121
|
|
|
144
|
-
# @return [Time]
|
|
145
122
|
def ctime
|
|
146
123
|
@ctime ||= Time.parse(self["ctime"])
|
|
147
124
|
end
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
6
|
-
# repository, or applies the given ones. Restic ignores
|
|
5
|
+
# +restic migrate+: checks which migrations can be applied to this
|
|
6
|
+
# repository, or applies the given ones. Restic ignores +--json+ for
|
|
7
7
|
# this command, so there's no programmatic way to discover which
|
|
8
|
-
# migrations are available
|
|
8
|
+
# migrations are available - run +restic migrate+ directly for that.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# +names:+ names the migrations to apply, or only lists available ones
|
|
11
|
+
# if empty. +force: true+ reapplies a migration already marked as
|
|
12
|
+
# applied.
|
|
13
13
|
def migrate(names: [], force: false)
|
|
14
14
|
argv = build_argv("migrate", names) do |a|
|
|
15
15
|
a.flag(:force) if force
|
|
@@ -4,27 +4,25 @@ require "open3"
|
|
|
4
4
|
|
|
5
5
|
module Yobi
|
|
6
6
|
class Repository
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# +restic mount+: serves this repository as a read-only FUSE filesystem
|
|
8
|
+
# at +mountpoint:+, which must already exist.
|
|
9
9
|
#
|
|
10
|
-
# Without a block, returns a
|
|
11
|
-
# itself ready; call
|
|
12
|
-
#
|
|
10
|
+
# Without a block, returns a Yobi::MountHandle once Restic reports
|
|
11
|
+
# itself ready; call +#stop+ yourself once done. With one, yields the
|
|
12
|
+
# MountHandle and stops it automatically once the block returns or
|
|
13
|
+
# raises, returning the block's own value.
|
|
13
14
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# @return [Yobi::MountHandle] if no block is given
|
|
26
|
-
# @return [Object] the block's own return value, otherwise
|
|
27
|
-
# @raise [Yobi::MountTimeout]
|
|
15
|
+
# +hosts:+, +paths:+, and +tags:+ each accept a single value or an
|
|
16
|
+
# Array to restrict which snapshots appear under +snapshots/+.
|
|
17
|
+
#
|
|
18
|
+
# +allow_other:+ allows other users to access the mount.
|
|
19
|
+
# +no_default_permissions:+ skips file permission checks.
|
|
20
|
+
# +owner_root:+ mounts files as owned by root. +path_templates:+ and
|
|
21
|
+
# +time_template:+ control the directory naming schemes under
|
|
22
|
+
# +snapshots/+.
|
|
23
|
+
#
|
|
24
|
+
# +ready_timeout:+ is the number of seconds to wait for Restic's
|
|
25
|
+
# readiness message before raising Yobi::MountTimeout.
|
|
28
26
|
def mount(mountpoint:, hosts: [], paths: [], tags: [], allow_other: false,
|
|
29
27
|
no_default_permissions: false, owner_root: false, path_templates: [], time_template: nil,
|
|
30
28
|
ready_timeout: 10)
|
|
@@ -58,20 +56,12 @@ module Yobi
|
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
class Restic
|
|
61
|
-
#
|
|
62
|
-
READY_LINE = "Now serving the repository at"
|
|
59
|
+
READY_LINE = "Now serving the repository at" # :nodoc:
|
|
63
60
|
|
|
64
|
-
# For
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# @param argv [Array<String>]
|
|
69
|
-
# @param mountpoint [String]
|
|
70
|
-
# @param extra_env [Hash{String => String}]
|
|
71
|
-
# @param ready_timeout [Numeric]
|
|
72
|
-
# @return [Yobi::MountHandle]
|
|
73
|
-
# @raise [Yobi::MountTimeout]
|
|
74
|
-
def run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10)
|
|
61
|
+
# For Yobi::Repository#mount. Spawns Restic and waits for its readiness
|
|
62
|
+
# line (or +ready_timeout:+ seconds, or Restic exiting on its own)
|
|
63
|
+
# before returning a Yobi::MountHandle. Raises Yobi::MountTimeout.
|
|
64
|
+
def run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10) # :nodoc:
|
|
75
65
|
ensure_minimum_version!
|
|
76
66
|
output = Yobi::ResticOutput.new
|
|
77
67
|
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
5
|
+
# +restic prune+: removes data no longer referenced by any snapshot.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
7
|
+
# +dry_run:+ reports what would happen without doing it. +max_unused:+
|
|
8
|
+
# targets a maximum unused space after pruning (e.g. +"10%"+).
|
|
9
|
+
# +max_repack_size:+ caps the amount of data repacked in one run.
|
|
10
|
+
#
|
|
11
|
+
# +repack_cacheable_only:+ only repacks packs cached locally.
|
|
12
|
+
# +repack_smaller_than:+ also repacks packs smaller than a given size.
|
|
13
|
+
# +repack_uncompressed:+ repacks packs not yet using compression.
|
|
14
|
+
#
|
|
15
|
+
# +unsafe_recover_no_free_space:+ proceeds even without enough free
|
|
16
|
+
# space, at the given risk acknowledgement string.
|
|
15
17
|
def prune(dry_run: false, max_repack_size: nil, max_unused: nil, repack_cacheable_only: false,
|
|
16
18
|
repack_smaller_than: nil, repack_uncompressed: false, unsafe_recover_no_free_space: nil)
|
|
17
19
|
argv = build_argv("prune") do |a|
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
5
|
+
# +restic recover+: builds a new snapshot from any data found in this
|
|
6
6
|
# repository that isn't referenced by an existing snapshot (e.g. after
|
|
7
|
-
# an accidental
|
|
7
|
+
# an accidental #forget). Call #snapshots afterward to find the
|
|
8
8
|
# recovered snapshot, if any was created.
|
|
9
|
-
#
|
|
10
|
-
# @return [true]
|
|
11
9
|
def recover
|
|
12
10
|
run_restic(build_argv("recover"))
|
|
13
11
|
true
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
5
|
+
# +restic repair index+: creates a new index based on the pack files
|
|
6
6
|
# present in this repository. Successor to the deprecated
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @param read_all_packs [Boolean] read every pack file fully instead of just headers
|
|
10
|
-
# @return [true]
|
|
7
|
+
# +rebuild-index+ command. +read_all_packs: true+ reads every pack file
|
|
8
|
+
# fully instead of just headers.
|
|
11
9
|
def repair_index(read_all_packs: false)
|
|
12
10
|
argv = build_argv("repair", "index") do |a|
|
|
13
11
|
a.flag(:read_all_packs) if read_all_packs
|
|
@@ -16,34 +14,29 @@ module Yobi
|
|
|
16
14
|
true
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# +restic repair packs+: extracts intact blobs from the given pack
|
|
18
|
+
# +ids:+, rebuilds the index to drop the damaged packs, and removes
|
|
21
19
|
# them from the repository. Restic also writes a backup copy of each
|
|
22
|
-
# given pack file (named
|
|
20
|
+
# given pack file (named +pack-<id>+) into the calling process's
|
|
23
21
|
# current working directory before removing it. There's no flag to
|
|
24
22
|
# disable this.
|
|
25
|
-
#
|
|
26
|
-
# @param ids [Array<String>, String] pack IDs
|
|
27
|
-
# @return [true]
|
|
28
23
|
def repair_packs(ids:)
|
|
29
24
|
argv = build_argv("repair", "packs", ids)
|
|
30
25
|
run_restic(argv)
|
|
31
26
|
true
|
|
32
27
|
end
|
|
33
28
|
|
|
34
|
-
#
|
|
35
|
-
# new ones with damaged directories/file contents removed. This
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# Depends on a correct index
|
|
29
|
+
# +restic repair snapshots+: scans the given snapshots and generates
|
|
30
|
+
# new ones with damaged directories/file contents removed. This causes
|
|
31
|
+
# data loss for the content actually removed; prefer a fresh
|
|
32
|
+
# Yobi::Repository#backup where the source data is still available.
|
|
33
|
+
# Depends on a correct index - call #repair_index first.
|
|
39
34
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# @param tags [Array<String>, String] filter by tag(s)
|
|
46
|
-
# @return [true]
|
|
35
|
+
# +snapshot_ids:+ names the snapshots to repair, or all of them if
|
|
36
|
+
# empty. +dry_run:+ reports what would happen without doing it.
|
|
37
|
+
# +forget:+ removes the original damaged snapshots after repairing.
|
|
38
|
+
# +hosts:+, +paths:+, and +tags:+ each accept a single value or an
|
|
39
|
+
# Array to filter by.
|
|
47
40
|
def repair_snapshots(snapshot_ids: [], dry_run: false, forget: false, hosts: [], paths: [], tags: [])
|
|
48
41
|
argv = build_argv("repair", "snapshots", snapshot_ids) do |a|
|
|
49
42
|
a.flag(:dry_run) if dry_run
|
|
@@ -2,32 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
4
|
class Repository
|
|
5
|
-
#
|
|
5
|
+
# +restic restore+: extracts a snapshot's contents to a target directory.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# @param verbose [Boolean] stream a {Yobi::RestoreVerboseStatus} per file to the block
|
|
28
|
-
# @param verify [Boolean] verify restored file content against the repository
|
|
29
|
-
# @yieldparam message [Yobi::RestoreStatus, Yobi::RestoreVerboseStatus]
|
|
30
|
-
# @return [Yobi::RestoreOutcome]
|
|
7
|
+
# +snapshot_id:+ is a snapshot ID or +"latest"+. +target:+ is the
|
|
8
|
+
# directory to restore into.
|
|
9
|
+
#
|
|
10
|
+
# +excludes:+, +exclude_files:+, +exclude_xattrs:+, +includes:+,
|
|
11
|
+
# +include_files:+, +include_xattrs:+ and their +i+-prefixed
|
|
12
|
+
# (case-insensitive) variants each accept a single value or an Array.
|
|
13
|
+
#
|
|
14
|
+
# +hosts:+, +paths:+ and +tags:+ each accept a single value or an
|
|
15
|
+
# Array and are only relevant when +snapshot_id+ is +"latest"+.
|
|
16
|
+
#
|
|
17
|
+
# +delete:+ deletes files in +target+ not present in the snapshot.
|
|
18
|
+
# +dry_run:+ reports what would happen without doing it.
|
|
19
|
+
# +overwrite:+ is +"always"+, +"if-changed"+, or +"if-newer"+.
|
|
20
|
+
# +ownership_by_name:+ maps ownership by user/group name instead of
|
|
21
|
+
# numeric ID. +sparse:+ writes sparse files. +verify:+ verifies restored
|
|
22
|
+
# file content against the repository.
|
|
23
|
+
#
|
|
24
|
+
# When +verbose: true+, a Yobi::RestoreVerboseStatus is streamed to the
|
|
25
|
+
# block per file. The block also receives Yobi::RestoreStatus messages
|
|
26
|
+
# as they arrive. Returns a Yobi::RestoreOutcome.
|
|
31
27
|
def restore(snapshot_id:, target:, excludes: [], exclude_files: [], exclude_xattrs: [], hosts: [],
|
|
32
28
|
iexcludes: [], iexclude_files: [], iincludes: [], iinclude_files: [], includes: [], include_files: [],
|
|
33
29
|
include_xattrs: [], paths: [], tags: [], delete: false, dry_run: false, overwrite: nil,
|
|
@@ -58,7 +54,7 @@ module Yobi
|
|
|
58
54
|
|
|
59
55
|
output = Yobi::ResticOutput.new(transform: Yobi::RestoreMessageWrapper)
|
|
60
56
|
# Restic's own restore summary isn't meant for the live block - only
|
|
61
|
-
#
|
|
57
|
+
# RestoreOutcome#summary's post-hoc access sees it - but it shares
|
|
62
58
|
# the same @transform (needed there since it isn't a status/verbose_status
|
|
63
59
|
# message), so it has to be filtered back out here instead.
|
|
64
60
|
live_block = block && proc { |message| block.call(message) if message.is_a?(RestoreStatus) || message.is_a?(RestoreVerboseStatus) }
|
|
@@ -67,16 +63,7 @@ module Yobi
|
|
|
67
63
|
end
|
|
68
64
|
end
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
# it in the matching typed class. Used both by {Yobi::Repository#restore}
|
|
72
|
-
# (as the {Yobi::ResticOutput} `transform:` for a live streaming run) and
|
|
73
|
-
# by {Yobi::RestoreOutcome}'s own post-hoc accessors.
|
|
74
|
-
#
|
|
75
|
-
# @private
|
|
76
|
-
module RestoreMessageWrapper
|
|
77
|
-
# @param raw [Hash]
|
|
78
|
-
# @return [Yobi::RestoreStatus, Yobi::RestoreVerboseStatus, Hash]
|
|
79
|
-
# the raw Hash itself for a message_type this version of Yobi doesn't recognize
|
|
66
|
+
module RestoreMessageWrapper # :nodoc:
|
|
80
67
|
def self.call(raw)
|
|
81
68
|
case raw["message_type"]
|
|
82
69
|
when "status" then RestoreStatus.new(raw)
|
|
@@ -86,23 +73,21 @@ module Yobi
|
|
|
86
73
|
end
|
|
87
74
|
end
|
|
88
75
|
|
|
89
|
-
# The outcome of one
|
|
76
|
+
# The outcome of one Yobi::Repository#restore call.
|
|
90
77
|
class RestoreOutcome
|
|
91
|
-
#
|
|
78
|
+
# The Yobi::ResticOutput backing this outcome.
|
|
92
79
|
attr_reader :output
|
|
93
80
|
|
|
94
|
-
#
|
|
95
|
-
def initialize(execution)
|
|
81
|
+
def initialize(execution) # :nodoc:
|
|
96
82
|
@output = execution[:output]
|
|
97
83
|
end
|
|
98
84
|
|
|
99
|
-
#
|
|
85
|
+
# Restic's own +"summary"+ fields (+"total_files"+, +"files_restored"+, ...).
|
|
100
86
|
def summary
|
|
101
87
|
@summary ||= output.messages("summary").first || {}
|
|
102
88
|
end
|
|
103
89
|
alias_method :report, :summary
|
|
104
90
|
|
|
105
|
-
# @return [void]
|
|
106
91
|
def pretty_print(q)
|
|
107
92
|
q.object_group(self) do
|
|
108
93
|
q.breakable
|
|
@@ -111,49 +96,41 @@ module Yobi
|
|
|
111
96
|
end
|
|
112
97
|
end
|
|
113
98
|
|
|
114
|
-
# One
|
|
115
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#restore
|
|
99
|
+
# One +"status"+ message from a live restore run. See
|
|
100
|
+
# https://restic.readthedocs.io/en/stable/075_scripting.html#restore.
|
|
116
101
|
class RestoreStatus < Yobi::FancyHash
|
|
117
|
-
# @return [Float, nil]
|
|
118
102
|
def percent_done
|
|
119
103
|
self["percent_done"]
|
|
120
104
|
end
|
|
121
105
|
|
|
122
|
-
# @return [Integer, nil]
|
|
123
106
|
def total_files
|
|
124
107
|
self["total_files"]
|
|
125
108
|
end
|
|
126
109
|
|
|
127
|
-
# @return [Integer, nil]
|
|
128
110
|
def files_restored
|
|
129
111
|
self["files_restored"]
|
|
130
112
|
end
|
|
131
113
|
|
|
132
|
-
# @return [Integer, nil]
|
|
133
114
|
def total_bytes
|
|
134
115
|
self["total_bytes"]
|
|
135
116
|
end
|
|
136
117
|
|
|
137
|
-
# @return [Integer, nil]
|
|
138
118
|
def bytes_restored
|
|
139
119
|
self["bytes_restored"]
|
|
140
120
|
end
|
|
141
121
|
end
|
|
142
122
|
|
|
143
|
-
# One
|
|
144
|
-
# emitted when
|
|
123
|
+
# One +"verbose_status"+ message from a restore run, one per file. Only
|
|
124
|
+
# emitted when +verbose: true+ is passed to Yobi::Repository#restore.
|
|
145
125
|
class RestoreVerboseStatus < Yobi::FancyHash
|
|
146
|
-
# @return [String]
|
|
147
126
|
def action
|
|
148
127
|
self["action"]
|
|
149
128
|
end
|
|
150
129
|
|
|
151
|
-
# @return [String]
|
|
152
130
|
def item
|
|
153
131
|
self["item"]
|
|
154
132
|
end
|
|
155
133
|
|
|
156
|
-
# @return [Integer]
|
|
157
134
|
def size
|
|
158
135
|
self["size"]
|
|
159
136
|
end
|