yobi 0.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +897 -0
- data/Rakefile +12 -0
- data/lib/yobi/argv_builder.rb +102 -0
- data/lib/yobi/errors.rb +109 -0
- data/lib/yobi/io_handle.rb +68 -0
- data/lib/yobi/mount.rb +66 -0
- data/lib/yobi/repository/backup.rb +310 -0
- data/lib/yobi/repository/cat.rb +87 -0
- data/lib/yobi/repository/check.rb +96 -0
- data/lib/yobi/repository/copy.rb +39 -0
- data/lib/yobi/repository/diff.rb +94 -0
- data/lib/yobi/repository/dump.rb +51 -0
- data/lib/yobi/repository/find.rb +160 -0
- data/lib/yobi/repository/forget.rb +164 -0
- data/lib/yobi/repository/key.rb +126 -0
- data/lib/yobi/repository/list.rb +17 -0
- data/lib/yobi/repository/ls.rb +138 -0
- data/lib/yobi/repository/migrate.rb +21 -0
- data/lib/yobi/repository/mount.rb +129 -0
- data/lib/yobi/repository/prune.rb +30 -0
- data/lib/yobi/repository/recover.rb +16 -0
- data/lib/yobi/repository/repair.rb +59 -0
- data/lib/yobi/repository/restore.rb +159 -0
- data/lib/yobi/repository/rewrite.rb +53 -0
- data/lib/yobi/repository/snapshots.rb +56 -0
- data/lib/yobi/repository/stats.rb +36 -0
- data/lib/yobi/repository/tag.rb +89 -0
- data/lib/yobi/repository/unlock.rb +17 -0
- data/lib/yobi/repository.rb +180 -0
- data/lib/yobi/restic.rb +341 -0
- data/lib/yobi/restic_output.rb +94 -0
- data/lib/yobi/snapshot.rb +45 -0
- data/lib/yobi/version.rb +8 -0
- data/lib/yobi.rb +13 -0
- data/sig/yobi.rbs +660 -0
- metadata +84 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "delegate"
|
|
6
|
+
|
|
7
|
+
module Yobi
|
|
8
|
+
class Repository
|
|
9
|
+
# `restic key add`: creates a new key (password) for this repository.
|
|
10
|
+
#
|
|
11
|
+
# @param host [String, nil] hostname to record on the new key
|
|
12
|
+
# @param user [String, nil] username to record on the new key
|
|
13
|
+
# @param new_insecure_no_password [Boolean] create the new key with an empty password
|
|
14
|
+
# @param new_password_file [String, nil] path to a file containing the new key's password
|
|
15
|
+
# @return [true]
|
|
16
|
+
def key_add(host: nil, user: nil, new_insecure_no_password: false, new_password_file: nil)
|
|
17
|
+
argv = build_argv("key", "add") do |a|
|
|
18
|
+
a.flag(:host, host) unless host.nil?
|
|
19
|
+
a.flag(:user, user) unless user.nil?
|
|
20
|
+
a.flag(:new_insecure_no_password) if new_insecure_no_password
|
|
21
|
+
a.flag(:new_password_file, new_password_file) unless new_password_file.nil?
|
|
22
|
+
end
|
|
23
|
+
run_restic(argv)
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
alias_method :add_key, :key_add
|
|
27
|
+
|
|
28
|
+
# `restic key list`: lists every key (password) associated with this repository.
|
|
29
|
+
#
|
|
30
|
+
# @return [Enumerable<Yobi::Key>]
|
|
31
|
+
def key_list
|
|
32
|
+
execution = run_restic(build_argv("key", "list"))
|
|
33
|
+
Keys.new(execution[:output])
|
|
34
|
+
end
|
|
35
|
+
alias_method :keys, :key_list
|
|
36
|
+
|
|
37
|
+
# `restic key passwd`: creates a new key for this repository and
|
|
38
|
+
# removes the key currently in use. Doesn't mutate this `Repository`
|
|
39
|
+
# instance's own `password:`. Build a new `Repository` with the
|
|
40
|
+
# rotated password to keep talking to this repository afterward.
|
|
41
|
+
#
|
|
42
|
+
# @param host [String, nil] hostname to record on the new key
|
|
43
|
+
# @param user [String, nil] username to record on the new key
|
|
44
|
+
# @param new_insecure_no_password [Boolean] set the new key's password to empty
|
|
45
|
+
# @param new_password_file [String, nil] path to a file containing the new key's password
|
|
46
|
+
# @return [true]
|
|
47
|
+
def key_passwd(host: nil, user: nil, new_insecure_no_password: false, new_password_file: nil)
|
|
48
|
+
argv = build_argv("key", "passwd") do |a|
|
|
49
|
+
a.flag(:host, host) unless host.nil?
|
|
50
|
+
a.flag(:user, user) unless user.nil?
|
|
51
|
+
a.flag(:new_insecure_no_password) if new_insecure_no_password
|
|
52
|
+
a.flag(:new_password_file, new_password_file) unless new_password_file.nil?
|
|
53
|
+
end
|
|
54
|
+
run_restic(argv)
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
alias_method :change_password, :key_passwd
|
|
58
|
+
|
|
59
|
+
# `restic key remove`: removes the given key ID from this repository.
|
|
60
|
+
# Restic refuses to remove the key currently being used to access the
|
|
61
|
+
# repository.
|
|
62
|
+
#
|
|
63
|
+
# @param id [String]
|
|
64
|
+
# @return [true]
|
|
65
|
+
def key_remove(id:)
|
|
66
|
+
run_restic(build_argv("key", "remove", id))
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
alias_method :remove_key, :key_remove
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# One key (password) associated with the repository, from {Yobi::Repository#key_list}.
|
|
73
|
+
class Key < SimpleDelegator
|
|
74
|
+
# @return [String]
|
|
75
|
+
def id
|
|
76
|
+
self["id"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [String]
|
|
80
|
+
def user_name
|
|
81
|
+
self["userName"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [String]
|
|
85
|
+
def host_name
|
|
86
|
+
self["hostName"]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Boolean] whether this is the key currently in use
|
|
90
|
+
def current?
|
|
91
|
+
self["current"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Time]
|
|
95
|
+
def created
|
|
96
|
+
@created ||= Time.parse(self["created"])
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Enumerable over every {Yobi::Key} in one {Yobi::Repository#key_list} call.
|
|
101
|
+
class Keys
|
|
102
|
+
include Enumerable
|
|
103
|
+
|
|
104
|
+
# @return [Yobi::ResticOutput]
|
|
105
|
+
attr_reader :output
|
|
106
|
+
|
|
107
|
+
# @param output [Yobi::ResticOutput]
|
|
108
|
+
def initialize(output)
|
|
109
|
+
@output = output
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @yieldparam key [Yobi::Key]
|
|
113
|
+
# @return [Enumerator] if no block is given
|
|
114
|
+
def each(&block)
|
|
115
|
+
return enum_for(:each) unless block_given?
|
|
116
|
+
|
|
117
|
+
keys.each(&block)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def keys
|
|
123
|
+
@keys ||= JSON.parse(output.to_s).map { |raw| Yobi::Key.new(raw) }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
class Repository
|
|
5
|
+
# `restic list`: lists every object ID of the given type in this
|
|
6
|
+
# repository. Restic ignores `--json` for this command; each line is a
|
|
7
|
+
# bare ID, except for `:blobs`, where each line is `"data <id>"` or `"tree <id>"`.
|
|
8
|
+
#
|
|
9
|
+
# @param type [Symbol, String] `:blobs`, `:packs`, `:index`, `:snapshots`, `:keys`, or `:locks`
|
|
10
|
+
# @return [Array<String>]
|
|
11
|
+
def list(type)
|
|
12
|
+
argv = build_argv("list", type)
|
|
13
|
+
execution = run_restic(argv)
|
|
14
|
+
execution[:output].to_s.each_line.map(&:strip).reject(&:empty?)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "delegate"
|
|
6
|
+
|
|
7
|
+
module Yobi
|
|
8
|
+
class Repository
|
|
9
|
+
# `restic ls`: lists a snapshot's files/directories.
|
|
10
|
+
#
|
|
11
|
+
# @param snapshot_id [String] also accepts `"latest"`
|
|
12
|
+
# @param dirs [Array<String>, String] restrict to these directories within the snapshot
|
|
13
|
+
# @param hosts [Array<String>, String] only relevant when `snapshot_id` is `"latest"`
|
|
14
|
+
# @param human_readable [Boolean] format sizes for humans
|
|
15
|
+
# @param long [Boolean] include long listing format
|
|
16
|
+
# @param paths [Array<String>, String] only relevant when `snapshot_id` is `"latest"`
|
|
17
|
+
# @param recursive [Boolean] recurse into subdirectories
|
|
18
|
+
# @param reverse [Boolean] reverse sort order
|
|
19
|
+
# @param sort [String, nil] sort key, e.g. `"size"`
|
|
20
|
+
# @param tags [Array<String>, String] only relevant when `snapshot_id` is `"latest"`
|
|
21
|
+
# @return [Yobi::LsOutcome]
|
|
22
|
+
def ls(snapshot_id:, dirs: [], hosts: [], human_readable: false, long: false, paths: [],
|
|
23
|
+
recursive: false, reverse: false, sort: nil, tags: [])
|
|
24
|
+
argv = build_argv("ls", snapshot_id) do |a|
|
|
25
|
+
a.repeat_flag(:host, hosts)
|
|
26
|
+
a.flag(:human_readable) if human_readable
|
|
27
|
+
a.flag(:long) if long
|
|
28
|
+
a.repeat_flag(:path, paths)
|
|
29
|
+
a.flag(:recursive) if recursive
|
|
30
|
+
a.flag(:reverse) if reverse
|
|
31
|
+
a.flag(:sort, sort) unless sort.nil?
|
|
32
|
+
a.repeat_flag(:tag, tags)
|
|
33
|
+
a.end_of_options.append(dirs)
|
|
34
|
+
end
|
|
35
|
+
execution = run_restic(argv)
|
|
36
|
+
LsOutcome.new(execution[:exit_code], execution[:output])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The outcome of one {Yobi::Repository#ls} call.
|
|
41
|
+
class LsOutcome < Struct.new(:exit_code, :output)
|
|
42
|
+
# @return [Yobi::Snapshot] the resolved snapshot, useful when `snapshot_id:` was `"latest"`
|
|
43
|
+
def snapshot
|
|
44
|
+
@snapshot ||= Yobi::Snapshot.new(JSON.parse(snapshot_line))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Enumerable<Yobi::LsEntry>]
|
|
48
|
+
def entries
|
|
49
|
+
@entries ||= LsEntries.new(output)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def snapshot_line
|
|
55
|
+
offset = output.index["snapshot"].first
|
|
56
|
+
output.read_line_at(offset)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# One file/directory entry from a {Yobi::Repository#ls} call.
|
|
61
|
+
class LsEntry < SimpleDelegator
|
|
62
|
+
# @return [String]
|
|
63
|
+
def name
|
|
64
|
+
self["name"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [String]
|
|
68
|
+
def type
|
|
69
|
+
self["type"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [String]
|
|
73
|
+
def path
|
|
74
|
+
self["path"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Integer]
|
|
78
|
+
def size
|
|
79
|
+
self["size"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [String]
|
|
83
|
+
def permissions
|
|
84
|
+
self["permissions"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Integer]
|
|
88
|
+
def uid
|
|
89
|
+
self["uid"]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Integer]
|
|
93
|
+
def gid
|
|
94
|
+
self["gid"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [Integer]
|
|
98
|
+
def inode
|
|
99
|
+
self["inode"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Time]
|
|
103
|
+
def mtime
|
|
104
|
+
@mtime ||= Time.parse(self["mtime"])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Time]
|
|
108
|
+
def atime
|
|
109
|
+
@atime ||= Time.parse(self["atime"])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Time]
|
|
113
|
+
def ctime
|
|
114
|
+
@ctime ||= Time.parse(self["ctime"])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Enumerable over every {Yobi::LsEntry} in a run.
|
|
119
|
+
class LsEntries
|
|
120
|
+
include Enumerable
|
|
121
|
+
|
|
122
|
+
# @param output [Yobi::ResticOutput]
|
|
123
|
+
def initialize(output)
|
|
124
|
+
@output = output
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @yieldparam entry [Yobi::LsEntry]
|
|
128
|
+
# @return [Enumerator] if no block is given
|
|
129
|
+
def each
|
|
130
|
+
return enum_for(:each) unless block_given?
|
|
131
|
+
|
|
132
|
+
@output.index["node"].each do |offset|
|
|
133
|
+
line = @output.read_line_at(offset)
|
|
134
|
+
yield LsEntry.new(JSON.parse(line))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
class Repository
|
|
5
|
+
# `restic migrate`: checks which migrations can be applied to this
|
|
6
|
+
# repository, or applies the given ones. Restic ignores `--json` for
|
|
7
|
+
# this command, so there's no programmatic way to discover which
|
|
8
|
+
# migrations are available. Run `restic migrate` directly for that.
|
|
9
|
+
#
|
|
10
|
+
# @param names [Array<String>, String] migration names to apply; only lists available ones if empty
|
|
11
|
+
# @param force [Boolean] reapply a migration already marked as applied
|
|
12
|
+
# @return [true]
|
|
13
|
+
def migrate(names: [], force: false)
|
|
14
|
+
argv = build_argv("migrate", names) do |a|
|
|
15
|
+
a.flag(:force) if force
|
|
16
|
+
end
|
|
17
|
+
run_restic(argv)
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module Yobi
|
|
7
|
+
class Repository
|
|
8
|
+
# `restic mount`: serves this repository as a read-only FUSE
|
|
9
|
+
# filesystem at `mountpoint`, which must already exist.
|
|
10
|
+
#
|
|
11
|
+
# Without a block, returns a {Yobi::Mount} once Restic reports itself
|
|
12
|
+
# ready; call `#stop` yourself once done. With one, yields the `Mount`
|
|
13
|
+
# and stops it automatically once the block returns or raises.
|
|
14
|
+
#
|
|
15
|
+
# @param mountpoint [String] must already exist
|
|
16
|
+
# @param hosts [Array<String>, String] restrict which snapshots appear under `snapshots/`
|
|
17
|
+
# @param paths [Array<String>, String] restrict which snapshots appear under `snapshots/`
|
|
18
|
+
# @param tags [Array<String>, String] restrict which snapshots appear under `snapshots/`
|
|
19
|
+
# @param allow_other [Boolean] allow other users to access the mount
|
|
20
|
+
# @param no_default_permissions [Boolean] don't check file permissions
|
|
21
|
+
# @param owner_root [Boolean] mount files as owned by root
|
|
22
|
+
# @param path_templates [Array<String>, String] directory naming scheme(s) under `snapshots/`
|
|
23
|
+
# @param time_template [String, nil] directory naming scheme for time-based paths
|
|
24
|
+
# @param ready_timeout [Numeric] seconds to wait for Restic's readiness message before raising
|
|
25
|
+
# @yieldparam mount [Yobi::Mount]
|
|
26
|
+
# @return [Yobi::Mount] if no block is given
|
|
27
|
+
# @return [Object] the block's own return value, otherwise
|
|
28
|
+
# @raise [Yobi::MountTimeout]
|
|
29
|
+
def mount(mountpoint:, hosts: [], paths: [], tags: [], allow_other: false,
|
|
30
|
+
no_default_permissions: false, owner_root: false, path_templates: [], time_template: nil,
|
|
31
|
+
ready_timeout: 10)
|
|
32
|
+
argv = build_argv("mount") do |a|
|
|
33
|
+
a.flag(:allow_other) if allow_other
|
|
34
|
+
a.repeat_flag(:host, hosts)
|
|
35
|
+
a.flag(:no_default_permissions) if no_default_permissions
|
|
36
|
+
a.flag(:owner_root) if owner_root
|
|
37
|
+
a.repeat_flag(:path, paths)
|
|
38
|
+
a.repeat_flag(:path_template, path_templates)
|
|
39
|
+
a.repeat_flag(:tag, tags)
|
|
40
|
+
a.flag(:time_template, time_template) unless time_template.nil?
|
|
41
|
+
a.end_of_options.append(mountpoint)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
mount = run_restic_mount(argv, mountpoint: mountpoint.to_s, ready_timeout: ready_timeout)
|
|
45
|
+
return mount unless block_given?
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
yield mount
|
|
49
|
+
ensure
|
|
50
|
+
mount.stop
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def run_restic_mount(argv, mountpoint:, ready_timeout:)
|
|
57
|
+
@restic.run_mount(argv, mountpoint: mountpoint, extra_env: env, ready_timeout: ready_timeout)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Restic
|
|
62
|
+
# Restic's own stdout line signaling a mount is ready to serve.
|
|
63
|
+
READY_LINE = "Now serving the repository at"
|
|
64
|
+
|
|
65
|
+
# For {Yobi::Repository#mount}. Spawns Restic and waits for its
|
|
66
|
+
# readiness line (or `ready_timeout:` seconds, or Restic exiting on
|
|
67
|
+
# its own) before returning.
|
|
68
|
+
#
|
|
69
|
+
# @param argv [Array<String>]
|
|
70
|
+
# @param mountpoint [String]
|
|
71
|
+
# @param extra_env [Hash{String => String}]
|
|
72
|
+
# @param ready_timeout [Numeric]
|
|
73
|
+
# @return [Yobi::Mount]
|
|
74
|
+
# @raise [Yobi::MountTimeout]
|
|
75
|
+
def run_mount(argv, mountpoint:, extra_env: {}, ready_timeout: 10)
|
|
76
|
+
ensure_minimum_version!
|
|
77
|
+
file = Tempfile.new("yobi-restic-output")
|
|
78
|
+
file.unlink
|
|
79
|
+
|
|
80
|
+
stdin, output, wait_thr = Open3.popen2e(env.merge(extra_env), restic_path, *argv)
|
|
81
|
+
stdin.close
|
|
82
|
+
|
|
83
|
+
case wait_for_ready(output, file, mountpoint, ready_timeout)
|
|
84
|
+
when :ready
|
|
85
|
+
Yobi::Mount.new(wait_thr: wait_thr, mountpoint: mountpoint, output: output, output_file: file, argv: argv)
|
|
86
|
+
when :timeout
|
|
87
|
+
Process.kill("INT", wait_thr.pid)
|
|
88
|
+
wait_thr.value
|
|
89
|
+
raise Yobi::MountTimeout.new(argv: argv, timeout: ready_timeout)
|
|
90
|
+
when :exited
|
|
91
|
+
status = wait_thr.value
|
|
92
|
+
self.class.dispatch(exit_code: status.exitstatus, output: Yobi::ResticOutput.new(file), argv: argv)
|
|
93
|
+
end
|
|
94
|
+
rescue Errno::ENOENT
|
|
95
|
+
file&.close
|
|
96
|
+
raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def wait_for_ready(output, file, mountpoint, timeout)
|
|
102
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
103
|
+
|
|
104
|
+
loop do
|
|
105
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
106
|
+
return :timeout if remaining <= 0
|
|
107
|
+
return :timeout if IO.select([output], nil, nil, remaining).nil?
|
|
108
|
+
|
|
109
|
+
line = output.gets
|
|
110
|
+
return :exited if line.nil?
|
|
111
|
+
|
|
112
|
+
file.write(line)
|
|
113
|
+
return (wait_for_mountpoint(mountpoint, deadline) ? :ready : :timeout) if line.include?(READY_LINE)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def wait_for_mountpoint(mountpoint, deadline)
|
|
118
|
+
until Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
119
|
+
begin
|
|
120
|
+
return true unless Dir.empty?(mountpoint)
|
|
121
|
+
rescue Errno::ENOENT, Errno::ENOTCONN, Errno::ENXIO, Errno::EIO
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
sleep 0.005
|
|
125
|
+
end
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
class Repository
|
|
5
|
+
# `restic prune`: removes data no longer referenced by any snapshot.
|
|
6
|
+
#
|
|
7
|
+
# @param dry_run [Boolean] report what would happen without doing it
|
|
8
|
+
# @param max_repack_size [String, nil] limit the amount of data repacked in this run
|
|
9
|
+
# @param max_unused [String, nil] target maximum unused space after pruning, e.g. `"10%"`
|
|
10
|
+
# @param repack_cacheable_only [Boolean] only repack packs that are cached locally
|
|
11
|
+
# @param repack_smaller_than [String, nil] also repack packs smaller than this size
|
|
12
|
+
# @param repack_uncompressed [Boolean] repack packs not yet using compression
|
|
13
|
+
# @param unsafe_recover_no_free_space [String, nil] proceed even without enough free space, at the given risk acknowledgement
|
|
14
|
+
# @return [true]
|
|
15
|
+
def prune(dry_run: false, max_repack_size: nil, max_unused: nil, repack_cacheable_only: false,
|
|
16
|
+
repack_smaller_than: nil, repack_uncompressed: false, unsafe_recover_no_free_space: nil)
|
|
17
|
+
argv = build_argv("prune") do |a|
|
|
18
|
+
a.flag(:dry_run) if dry_run
|
|
19
|
+
a.flag(:max_repack_size, max_repack_size) unless max_repack_size.nil?
|
|
20
|
+
a.flag(:max_unused, max_unused) unless max_unused.nil?
|
|
21
|
+
a.flag(:repack_cacheable_only) if repack_cacheable_only
|
|
22
|
+
a.flag(:repack_smaller_than, repack_smaller_than) unless repack_smaller_than.nil?
|
|
23
|
+
a.flag(:repack_uncompressed) if repack_uncompressed
|
|
24
|
+
a.flag(:unsafe_recover_no_free_space, unsafe_recover_no_free_space) unless unsafe_recover_no_free_space.nil?
|
|
25
|
+
end
|
|
26
|
+
run_restic(argv)
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
class Repository
|
|
5
|
+
# `restic recover`: builds a new snapshot from any data found in this
|
|
6
|
+
# repository that isn't referenced by an existing snapshot (e.g. after
|
|
7
|
+
# an accidental {#forget}). Call {#snapshots} afterward to find the
|
|
8
|
+
# recovered snapshot, if any was created.
|
|
9
|
+
#
|
|
10
|
+
# @return [true]
|
|
11
|
+
def recover
|
|
12
|
+
run_restic(build_argv("recover"))
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
class Repository
|
|
5
|
+
# `restic repair index`: creates a new index based on the pack files
|
|
6
|
+
# present in this repository. Successor to the deprecated
|
|
7
|
+
# `rebuild-index` command.
|
|
8
|
+
#
|
|
9
|
+
# @param read_all_packs [Boolean] read every pack file fully instead of just headers
|
|
10
|
+
# @return [true]
|
|
11
|
+
def repair_index(read_all_packs: false)
|
|
12
|
+
argv = build_argv("repair", "index") do |a|
|
|
13
|
+
a.flag(:read_all_packs) if read_all_packs
|
|
14
|
+
end
|
|
15
|
+
run_restic(argv)
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# `restic repair packs`: extracts intact blobs from the given pack
|
|
20
|
+
# files, rebuilds the index to drop the damaged packs, and removes
|
|
21
|
+
# them from the repository. Restic also writes a backup copy of each
|
|
22
|
+
# given pack file (named `pack-<id>`) into the calling process's
|
|
23
|
+
# current working directory before removing it. There's no flag to
|
|
24
|
+
# disable this.
|
|
25
|
+
#
|
|
26
|
+
# @param ids [Array<String>, String] pack IDs
|
|
27
|
+
# @return [true]
|
|
28
|
+
def repair_packs(ids:)
|
|
29
|
+
argv = build_argv("repair", "packs", ids)
|
|
30
|
+
run_restic(argv)
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `restic repair snapshots`: scans the given snapshots and generates
|
|
35
|
+
# new ones with damaged directories/file contents removed. This
|
|
36
|
+
# causes data loss for the content actually removed; prefer a fresh
|
|
37
|
+
# {Yobi::Repository#backup} where the source data is still available.
|
|
38
|
+
# Depends on a correct index. Call {#repair_index} first.
|
|
39
|
+
#
|
|
40
|
+
# @param snapshot_ids [Array<String>, String] snapshots to repair; all of them if empty
|
|
41
|
+
# @param dry_run [Boolean] report what would happen without doing it
|
|
42
|
+
# @param forget [Boolean] remove the original damaged snapshots after repairing
|
|
43
|
+
# @param hosts [Array<String>, String] filter by hostname(s)
|
|
44
|
+
# @param paths [Array<String>, String] filter by originally backed-up path(s)
|
|
45
|
+
# @param tags [Array<String>, String] filter by tag(s)
|
|
46
|
+
# @return [true]
|
|
47
|
+
def repair_snapshots(snapshot_ids: [], dry_run: false, forget: false, hosts: [], paths: [], tags: [])
|
|
48
|
+
argv = build_argv("repair", "snapshots", snapshot_ids) do |a|
|
|
49
|
+
a.flag(:dry_run) if dry_run
|
|
50
|
+
a.flag(:forget) if forget
|
|
51
|
+
a.repeat_flag(:host, hosts)
|
|
52
|
+
a.repeat_flag(:path, paths)
|
|
53
|
+
a.repeat_flag(:tag, tags)
|
|
54
|
+
end
|
|
55
|
+
run_restic(argv)
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|