yobi 0.1.0 → 0.2.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/CHANGELOG.md +16 -0
- data/README.md +93 -124
- data/lib/yobi/argv_builder.rb +5 -0
- data/lib/yobi/errors.rb +6 -10
- data/lib/yobi/fancy_hash.rb +31 -0
- data/lib/yobi/io_handle.rb +9 -7
- data/lib/yobi/{mount.rb → mount_handle.rb} +11 -10
- data/lib/yobi/repository/backup.rb +75 -85
- data/lib/yobi/repository/cat.rb +10 -0
- data/lib/yobi/repository/check.rb +61 -45
- data/lib/yobi/repository/diff.rb +130 -47
- data/lib/yobi/repository/dump.rb +1 -1
- data/lib/yobi/repository/find.rb +11 -33
- data/lib/yobi/repository/forget.rb +6 -48
- data/lib/yobi/repository/init.rb +70 -0
- data/lib/yobi/repository/key.rb +59 -52
- data/lib/yobi/repository/ls.rb +48 -37
- data/lib/yobi/repository/mount.rb +16 -18
- data/lib/yobi/repository/restore.rb +42 -40
- data/lib/yobi/repository/snapshots.rb +2 -29
- data/lib/yobi/repository/stats.rb +47 -4
- data/lib/yobi/repository/tag.rb +50 -35
- data/lib/yobi/repository.rb +8 -43
- data/lib/yobi/restic.rb +73 -32
- data/lib/yobi/restic_output.rb +169 -12
- data/lib/yobi/snapshot.rb +81 -2
- data/lib/yobi/version.rb +2 -2
- data/lib/yobi.rb +2 -1
- data/sig/yobi.rbs +252 -139
- metadata +5 -7
- data/Rakefile +0 -12
data/lib/yobi/repository/diff.rb
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "delegate"
|
|
5
|
-
|
|
6
3
|
module Yobi
|
|
7
4
|
class Repository
|
|
8
5
|
# `restic diff`: compares two snapshots.
|
|
@@ -15,51 +12,66 @@ module Yobi
|
|
|
15
12
|
argv = build_argv("diff", from, to) do |a|
|
|
16
13
|
a.flag(:metadata) if metadata
|
|
17
14
|
end
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
output = Yobi::ResticOutput.new(transform: Yobi::DiffMessageWrapper)
|
|
16
|
+
execution = run_restic(argv, output: output)
|
|
17
|
+
DiffOutcome.new(execution)
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
# Classifies a raw diff message Hash by its own message_type, wrapping
|
|
22
|
+
# it in the matching typed class. Used both as the {Yobi::ResticOutput}
|
|
23
|
+
# `transform:` for {Yobi::Repository#diff} and by {Yobi::DiffOutcome}'s
|
|
24
|
+
# own post-hoc accessors.
|
|
25
|
+
#
|
|
26
|
+
# @private
|
|
27
|
+
module DiffMessageWrapper
|
|
28
|
+
# @param raw [Hash]
|
|
29
|
+
# @return [Yobi::DiffChange, Yobi::DiffStatistics, Hash]
|
|
30
|
+
# the raw Hash itself for a message_type this version of Yobi doesn't recognize
|
|
31
|
+
def self.call(raw)
|
|
32
|
+
case raw["message_type"]
|
|
33
|
+
when "change" then DiffChange.new(raw)
|
|
34
|
+
when "statistics" then DiffStatistics.new(raw)
|
|
35
|
+
else raw
|
|
36
|
+
end
|
|
28
37
|
end
|
|
38
|
+
end
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
# The outcome of one {Yobi::Repository#diff} call.
|
|
41
|
+
class DiffOutcome
|
|
42
|
+
# @return [Yobi::ResticOutput]
|
|
43
|
+
attr_reader :output
|
|
34
44
|
|
|
35
|
-
private
|
|
45
|
+
# @private
|
|
46
|
+
def initialize(execution)
|
|
47
|
+
@output = execution[:output]
|
|
48
|
+
end
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if hash && hash["message_type"] == "statistics"
|
|
41
|
-
hash
|
|
42
|
-
else
|
|
43
|
-
find_summary || {}
|
|
44
|
-
end
|
|
50
|
+
# @return [Yobi::DiffStatistics] Restic's own `"statistics"` fields
|
|
51
|
+
def statistics
|
|
52
|
+
@statistics ||= output.messages("statistics").first || DiffStatistics.new({})
|
|
45
53
|
end
|
|
54
|
+
alias_method :report, :statistics
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
# @return [Enumerable<Yobi::DiffChange>]
|
|
57
|
+
def changes
|
|
58
|
+
@changes ||= output.messages("change")
|
|
59
|
+
end
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
# @return [void]
|
|
62
|
+
def pretty_print(q)
|
|
63
|
+
q.object_group(self) do
|
|
64
|
+
q.breakable
|
|
65
|
+
q.pp statistics
|
|
53
66
|
end
|
|
54
|
-
|
|
55
|
-
nil
|
|
56
67
|
end
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
# One `"change"` message from a diff run. `modifier` is Restic's own
|
|
60
|
-
# single-character
|
|
61
|
-
# updated, `"M"` content modified, `"T"` type changed,
|
|
62
|
-
|
|
71
|
+
# concatenation of single-character codes: `"+"` added, `"-"` removed,
|
|
72
|
+
# `"U"` metadata updated, `"M"` content modified, `"T"` type changed,
|
|
73
|
+
# `"?"` bitrot detected.
|
|
74
|
+
class DiffChange < Yobi::FancyHash
|
|
63
75
|
# @return [String]
|
|
64
76
|
def path
|
|
65
77
|
self["path"]
|
|
@@ -69,26 +81,97 @@ module Yobi
|
|
|
69
81
|
def modifier
|
|
70
82
|
self["modifier"]
|
|
71
83
|
end
|
|
84
|
+
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def added?
|
|
87
|
+
modifier.include?("+")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def removed?
|
|
92
|
+
modifier.include?("-")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def type_changed?
|
|
97
|
+
modifier.include?("T")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def modified?
|
|
102
|
+
modifier.include?("M")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def metadata_updated?
|
|
107
|
+
modifier.include?("U")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def bitrot?
|
|
112
|
+
modifier.include?("?")
|
|
113
|
+
end
|
|
72
114
|
end
|
|
73
115
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
# The `"statistics"` message from a diff run, the final result once the
|
|
117
|
+
# command finishes. Dispatched to {Yobi::DiffOutcome#statistics}.
|
|
118
|
+
class DiffStatistics < Yobi::FancyHash
|
|
119
|
+
# @return [String, nil]
|
|
120
|
+
def source_snapshot
|
|
121
|
+
self["source_snapshot"]
|
|
122
|
+
end
|
|
77
123
|
|
|
78
|
-
# @
|
|
79
|
-
def
|
|
80
|
-
|
|
124
|
+
# @return [String, nil]
|
|
125
|
+
def target_snapshot
|
|
126
|
+
self["target_snapshot"]
|
|
81
127
|
end
|
|
82
128
|
|
|
83
|
-
# @
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
129
|
+
# @return [Integer]
|
|
130
|
+
def changed_files
|
|
131
|
+
self["changed_files"] || 0
|
|
132
|
+
end
|
|
87
133
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
# @return [Yobi::DiffStat]
|
|
135
|
+
def added
|
|
136
|
+
@added ||= DiffStat.new(self["added"] || {})
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [Yobi::DiffStat]
|
|
140
|
+
def removed
|
|
141
|
+
@removed ||= DiffStat.new(self["removed"] || {})
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The `"added"`/`"removed"` sub-object of a {Yobi::DiffStatistics} message.
|
|
146
|
+
class DiffStat < Yobi::FancyHash
|
|
147
|
+
# @return [Integer]
|
|
148
|
+
def files
|
|
149
|
+
self["files"] || 0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @return [Integer]
|
|
153
|
+
def dirs
|
|
154
|
+
self["dirs"] || 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @return [Integer]
|
|
158
|
+
def others
|
|
159
|
+
self["others"] || 0
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [Integer]
|
|
163
|
+
def data_blobs
|
|
164
|
+
self["data_blobs"] || 0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [Integer]
|
|
168
|
+
def tree_blobs
|
|
169
|
+
self["tree_blobs"] || 0
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @return [Integer]
|
|
173
|
+
def bytes
|
|
174
|
+
self["bytes"] || 0
|
|
92
175
|
end
|
|
93
176
|
end
|
|
94
177
|
end
|
data/lib/yobi/repository/dump.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "tempfile"
|
|
|
4
4
|
|
|
5
5
|
module Yobi
|
|
6
6
|
class Repository
|
|
7
|
-
#
|
|
7
|
+
# @private
|
|
8
8
|
ARCHIVE_TYPES = Set.new(%w[tar zip].flat_map { [_1, _1.to_sym] }).freeze
|
|
9
9
|
|
|
10
10
|
# `restic dump`: extracts a file/folder from a snapshot. A single file's
|
data/lib/yobi/repository/find.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "time"
|
|
5
|
-
require "delegate"
|
|
6
5
|
|
|
7
6
|
module Yobi
|
|
8
7
|
class Repository
|
|
@@ -24,7 +23,7 @@ module Yobi
|
|
|
24
23
|
# @param show_pack_id [Boolean] include the pack ID each match is stored in
|
|
25
24
|
# @param snapshot_ids [Array<String>, String] restrict to these snapshot ID(s)
|
|
26
25
|
# @param tags [Array<String>, String] filter by tag(s)
|
|
27
|
-
# @return [
|
|
26
|
+
# @return [Array<Yobi::MatchesPerSnapshot>]
|
|
28
27
|
def find(patterns:, blob: false, pack: false, tree: false, hosts: [], human_readable: false,
|
|
29
28
|
ignore_case: false, long: false, newest: nil, oldest: nil, paths: [], reverse: false,
|
|
30
29
|
show_pack_id: false, snapshot_ids: [], tags: [])
|
|
@@ -46,12 +45,12 @@ module Yobi
|
|
|
46
45
|
a.end_of_options.append(patterns)
|
|
47
46
|
end
|
|
48
47
|
execution = run_restic(argv)
|
|
49
|
-
|
|
48
|
+
JSON.parse(execution[:output].to_s).map { |raw| MatchesPerSnapshot.new(raw) }
|
|
50
49
|
end
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
# One matched file/directory from a {Yobi::Repository#find} call.
|
|
54
|
-
class FindMatch <
|
|
53
|
+
class FindMatch < Yobi::FancyHash
|
|
55
54
|
# @return [String]
|
|
56
55
|
def path
|
|
57
56
|
self["path"]
|
|
@@ -114,11 +113,17 @@ module Yobi
|
|
|
114
113
|
end
|
|
115
114
|
|
|
116
115
|
# One snapshot's worth of matches from a {Yobi::Repository#find} call.
|
|
117
|
-
class MatchesPerSnapshot <
|
|
118
|
-
#
|
|
116
|
+
class MatchesPerSnapshot < Yobi::FancyHash
|
|
117
|
+
# Restic's own `find --json` output only gives the snapshot's ID here,
|
|
118
|
+
# not a full record - unlike every other place in this API where
|
|
119
|
+
# "snapshot" means a {Yobi::Snapshot}. Call {Yobi::Repository#snapshots}
|
|
120
|
+
# yourself if you need the full record.
|
|
121
|
+
#
|
|
122
|
+
# @return [String]
|
|
119
123
|
def snapshot
|
|
120
124
|
self["snapshot"]
|
|
121
125
|
end
|
|
126
|
+
alias_method :snapshot_id, :snapshot
|
|
122
127
|
|
|
123
128
|
# @return [Integer]
|
|
124
129
|
def hits
|
|
@@ -130,31 +135,4 @@ module Yobi
|
|
|
130
135
|
@matches ||= (self["matches"] || []).map { |raw| Yobi::FindMatch.new(raw) }
|
|
131
136
|
end
|
|
132
137
|
end
|
|
133
|
-
|
|
134
|
-
# Enumerable over every {Yobi::MatchesPerSnapshot} in one {Yobi::Repository#find} call.
|
|
135
|
-
class FindMatches
|
|
136
|
-
include Enumerable
|
|
137
|
-
|
|
138
|
-
# @return [Yobi::ResticOutput]
|
|
139
|
-
attr_reader :output
|
|
140
|
-
|
|
141
|
-
# @param output [Yobi::ResticOutput]
|
|
142
|
-
def initialize(output)
|
|
143
|
-
@output = output
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# @yieldparam matches [Yobi::MatchesPerSnapshot]
|
|
147
|
-
# @return [Enumerator] if no block is given
|
|
148
|
-
def each(&block)
|
|
149
|
-
return enum_for(:each) unless block_given?
|
|
150
|
-
|
|
151
|
-
matches_per_snapshot.each(&block)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
def matches_per_snapshot
|
|
157
|
-
@matches_per_snapshot ||= JSON.parse(output.to_s).map { |raw| Yobi::MatchesPerSnapshot.new(raw) }
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
138
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "delegate"
|
|
5
4
|
|
|
6
5
|
module Yobi
|
|
7
6
|
class Repository
|
|
@@ -34,7 +33,7 @@ module Yobi
|
|
|
34
33
|
# @param repack_cacheable_only [Boolean] passed through to the implied prune
|
|
35
34
|
# @param repack_uncompressed [Boolean] passed through to the implied prune
|
|
36
35
|
# @param repack_smaller_than [String, nil] passed through to the implied prune
|
|
37
|
-
# @return [
|
|
36
|
+
# @return [Array<Yobi::ForgetGroup>]
|
|
38
37
|
def forget(keep_last: nil, keep_hourly: nil, keep_daily: nil, keep_weekly: nil, keep_monthly: nil,
|
|
39
38
|
keep_yearly: nil, keep_within: nil, keep_within_hourly: nil, keep_within_daily: nil,
|
|
40
39
|
keep_within_weekly: nil, keep_within_monthly: nil, keep_within_yearly: nil, keep_tags: [],
|
|
@@ -70,54 +69,13 @@ module Yobi
|
|
|
70
69
|
a.flag(:repack_smaller_than, repack_smaller_than) unless repack_smaller_than.nil?
|
|
71
70
|
end
|
|
72
71
|
execution = run_restic(argv)
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# The outcome of one {Yobi::Repository#forget} call.
|
|
78
|
-
class ForgetGroups
|
|
79
|
-
include Enumerable
|
|
80
|
-
|
|
81
|
-
# @return [Integer]
|
|
82
|
-
attr_reader :exit_code
|
|
83
|
-
# @return [Yobi::ResticOutput]
|
|
84
|
-
attr_reader :output
|
|
85
|
-
|
|
86
|
-
# @param exit_code [Integer]
|
|
87
|
-
# @param output [Yobi::ResticOutput]
|
|
88
|
-
def initialize(exit_code, output)
|
|
89
|
-
@exit_code = exit_code
|
|
90
|
-
@output = output
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# @yieldparam group [Yobi::ForgetGroup]
|
|
94
|
-
# @return [Enumerator] if no block is given
|
|
95
|
-
def each(&block)
|
|
96
|
-
return enum_for(:each) unless block_given?
|
|
97
|
-
|
|
98
|
-
groups.each(&block)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# @return [Boolean]
|
|
102
|
-
def success?
|
|
103
|
-
exit_code.zero?
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# @return [Boolean]
|
|
107
|
-
def partial?
|
|
108
|
-
exit_code == 3
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def groups
|
|
114
|
-
@groups ||= JSON.parse(output.to_s).map { |raw| Yobi::ForgetGroup.new(raw) }
|
|
72
|
+
JSON.parse(execution[:output].to_s).map { |raw| ForgetGroup.new(raw) }
|
|
115
73
|
end
|
|
116
74
|
end
|
|
117
75
|
|
|
118
76
|
# One grouping Restic's forget policy was evaluated against (by default,
|
|
119
77
|
# grouped by host+paths).
|
|
120
|
-
class ForgetGroup <
|
|
78
|
+
class ForgetGroup < Yobi::FancyHash
|
|
121
79
|
# @return [String]
|
|
122
80
|
def host
|
|
123
81
|
self["host"]
|
|
@@ -143,14 +101,14 @@ module Yobi
|
|
|
143
101
|
@remove ||= (self["remove"] || []).map { |entry| Yobi::Snapshot.new(entry) }
|
|
144
102
|
end
|
|
145
103
|
|
|
146
|
-
# @return [Array<Yobi::
|
|
104
|
+
# @return [Array<Yobi::KeepReason>] one per kept snapshot, explaining which policy rule(s) kept it
|
|
147
105
|
def reasons
|
|
148
|
-
@reasons ||= (self["reasons"] || []).map { |entry| Yobi::
|
|
106
|
+
@reasons ||= (self["reasons"] || []).map { |entry| Yobi::KeepReason.new(entry) }
|
|
149
107
|
end
|
|
150
108
|
end
|
|
151
109
|
|
|
152
110
|
# Why one snapshot survived a {Yobi::Repository#forget} run.
|
|
153
|
-
class
|
|
111
|
+
class KeepReason < Yobi::FancyHash
|
|
154
112
|
# @return [Yobi::Snapshot]
|
|
155
113
|
def snapshot
|
|
156
114
|
@snapshot ||= Yobi::Snapshot.new(self["snapshot"])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Yobi
|
|
6
|
+
class Repository
|
|
7
|
+
# `restic init`: creates the repository at {#url}.
|
|
8
|
+
#
|
|
9
|
+
# @param copy_chunker_params [Boolean] copy chunker parameters from `from_repo:`/`from_repository_file:`
|
|
10
|
+
# @param from_key_hint [String, nil]
|
|
11
|
+
# @param from_password [String, Array, Symbol, #call, nil] the source
|
|
12
|
+
# repository's password (when copying chunker params from one), same
|
|
13
|
+
# shape as {#initialize}'s `password:`
|
|
14
|
+
# @param from_repo [String, Yobi::Repository, nil] the source repository, or its URL
|
|
15
|
+
# (given a `Repository`, its own `#url`/`#password` are used automatically)
|
|
16
|
+
# @param from_repository_file [String, nil]
|
|
17
|
+
# @param repository_version [String, nil]
|
|
18
|
+
# @return [Yobi::Initialized]
|
|
19
|
+
def init(copy_chunker_params: false, from_key_hint: nil, from_password: nil, from_repo: nil,
|
|
20
|
+
from_repository_file: nil, repository_version: nil)
|
|
21
|
+
if from_repo.is_a?(Repository)
|
|
22
|
+
from_password = from_repo.password if from_password.nil?
|
|
23
|
+
from_repo = from_repo.url
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
argv = build_argv("init") do |a|
|
|
27
|
+
a.flag(:copy_chunker_params) if copy_chunker_params
|
|
28
|
+
a.flag(:from_insecure_no_password) if from_password == :insecure_no_password
|
|
29
|
+
a.flag(:from_key_hint, from_key_hint) unless from_key_hint.nil?
|
|
30
|
+
a.flag(:from_repo, from_repo) unless from_repo.nil?
|
|
31
|
+
a.flag(:from_repository_file, from_repository_file) unless from_repository_file.nil?
|
|
32
|
+
a.flag(:repository_version, repository_version) unless repository_version.nil?
|
|
33
|
+
end
|
|
34
|
+
execution = run_restic(argv, extra_env: password_env(from_password, "RESTIC_FROM_PASSWORD"))
|
|
35
|
+
Initialized.new(JSON.parse(execution[:output].to_s))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Constructs a new {Yobi::Repository} at `url`, already initialized with
|
|
39
|
+
# chunker parameters copied from this one (`{#init}(copy_chunker_params:
|
|
40
|
+
# true, from_repo: self)`), so a later {#copy} between the two can
|
|
41
|
+
# deduplicate. A one-time setup call, not an ongoing sync: nothing keeps
|
|
42
|
+
# the two repositories in sync afterward, and this doesn't mutate this
|
|
43
|
+
# `Repository` instance or copy any snapshots itself.
|
|
44
|
+
#
|
|
45
|
+
# @param url [String] the new repository's location
|
|
46
|
+
# @param password [String, Array, Symbol, #call] the new repository's own password
|
|
47
|
+
# @param backend_credentials [Hash{String => String}, #call] the new repository's own backend credentials
|
|
48
|
+
# @param restic [Yobi::Restic, String, nil] a `Restic` instance to share, defaults to this repository's own
|
|
49
|
+
# @param repository_version [String, nil]
|
|
50
|
+
# @return [Yobi::Repository] the newly initialized mirror
|
|
51
|
+
def init_mirror(url:, password:, backend_credentials: {}, restic: @restic, repository_version: nil)
|
|
52
|
+
Repository.new(url: url, password: password, backend_credentials: backend_credentials, restic: restic).tap do |mirror|
|
|
53
|
+
mirror.init(copy_chunker_params: true, from_repo: self, repository_version: repository_version)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The `"initialized"` message from an {Yobi::Repository#init} run.
|
|
59
|
+
class Initialized < Yobi::FancyHash
|
|
60
|
+
# @return [String] ID of the created repository
|
|
61
|
+
def id
|
|
62
|
+
self["id"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [String] URL of the repository
|
|
66
|
+
def repository
|
|
67
|
+
self["repository"]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/yobi/repository/key.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "time"
|
|
5
|
-
require "
|
|
5
|
+
require "tempfile"
|
|
6
6
|
|
|
7
7
|
module Yobi
|
|
8
8
|
class Repository
|
|
@@ -10,48 +10,56 @@ module Yobi
|
|
|
10
10
|
#
|
|
11
11
|
# @param host [String, nil] hostname to record on the new key
|
|
12
12
|
# @param user [String, nil] username to record on the new key
|
|
13
|
-
# @param
|
|
14
|
-
#
|
|
13
|
+
# @param new_password [String, Array, Symbol, #call] the new key's password,
|
|
14
|
+
# accepting the same shapes as {#initialize}'s `password:` minus `[:command, ...]`: a
|
|
15
|
+
# literal String; a `[:file, "..."]` tuple, resolved natively by Restic
|
|
16
|
+
# itself; `:insecure_no_password`; or anything responding to `#call`. A
|
|
17
|
+
# literal String or callable is written to a briefly-lived,
|
|
18
|
+
# 0600-permissioned tempfile, since Restic itself only accepts a new
|
|
19
|
+
# password by file or interactive prompt, unlike the repository's own
|
|
20
|
+
# `password:`
|
|
15
21
|
# @return [true]
|
|
16
|
-
def key_add(host: nil, user: nil
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
def key_add(new_password:, host: nil, user: nil)
|
|
23
|
+
with_new_password_flag(new_password) do |flag_name, flag_value|
|
|
24
|
+
argv = build_argv("key", "add") do |a|
|
|
25
|
+
a.flag(:host, host) unless host.nil?
|
|
26
|
+
a.flag(:user, user) unless user.nil?
|
|
27
|
+
a.flag(flag_name, flag_value) unless flag_name.nil?
|
|
28
|
+
end
|
|
29
|
+
run_restic(argv)
|
|
22
30
|
end
|
|
23
|
-
run_restic(argv)
|
|
24
31
|
true
|
|
25
32
|
end
|
|
26
33
|
alias_method :add_key, :key_add
|
|
27
34
|
|
|
28
35
|
# `restic key list`: lists every key (password) associated with this repository.
|
|
29
36
|
#
|
|
30
|
-
# @return [
|
|
37
|
+
# @return [Array<Yobi::Key>]
|
|
31
38
|
def key_list
|
|
32
39
|
execution = run_restic(build_argv("key", "list"))
|
|
33
|
-
|
|
40
|
+
JSON.parse(execution[:output].to_s).map { |raw| Key.new(raw) }
|
|
34
41
|
end
|
|
35
42
|
alias_method :keys, :key_list
|
|
36
43
|
|
|
37
44
|
# `restic key passwd`: creates a new key for this repository and
|
|
38
|
-
# removes the key currently in use.
|
|
39
|
-
# instance's own `password
|
|
40
|
-
#
|
|
45
|
+
# removes the key currently in use. On success, also updates this
|
|
46
|
+
# `Repository` instance's own `password:` to `new_password:`, so it
|
|
47
|
+
# keeps working against this same repository afterward.
|
|
41
48
|
#
|
|
42
49
|
# @param host [String, nil] hostname to record on the new key
|
|
43
50
|
# @param user [String, nil] username to record on the new key
|
|
44
|
-
# @param
|
|
45
|
-
# @param new_password_file [String, nil] path to a file containing the new key's password
|
|
51
|
+
# @param new_password [String, Array, Symbol, #call] see {#key_add}
|
|
46
52
|
# @return [true]
|
|
47
|
-
def key_passwd(host: nil, user: nil
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
def key_passwd(new_password:, host: nil, user: nil)
|
|
54
|
+
with_new_password_flag(new_password) do |flag_name, flag_value|
|
|
55
|
+
argv = build_argv("key", "passwd") do |a|
|
|
56
|
+
a.flag(:host, host) unless host.nil?
|
|
57
|
+
a.flag(:user, user) unless user.nil?
|
|
58
|
+
a.flag(flag_name, flag_value) unless flag_name.nil?
|
|
59
|
+
end
|
|
60
|
+
run_restic(argv)
|
|
53
61
|
end
|
|
54
|
-
|
|
62
|
+
@password = new_password
|
|
55
63
|
true
|
|
56
64
|
end
|
|
57
65
|
alias_method :change_password, :key_passwd
|
|
@@ -67,10 +75,36 @@ module Yobi
|
|
|
67
75
|
true
|
|
68
76
|
end
|
|
69
77
|
alias_method :remove_key, :key_remove
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def with_new_password_flag(new_password)
|
|
82
|
+
case new_password
|
|
83
|
+
in :insecure_no_password
|
|
84
|
+
yield :new_insecure_no_password, nil
|
|
85
|
+
in [:file, String => path]
|
|
86
|
+
yield :new_password_file, path
|
|
87
|
+
in String => literal
|
|
88
|
+
with_temp_password_file(literal) { |path| yield :new_password_file, path }
|
|
89
|
+
in callable if callable.respond_to?(:call)
|
|
90
|
+
with_temp_password_file(callable.call.to_s) { |path| yield :new_password_file, path }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def with_temp_password_file(password)
|
|
95
|
+
file = Tempfile.new("yobi-new-password")
|
|
96
|
+
file.chmod(0o600)
|
|
97
|
+
file.write(password)
|
|
98
|
+
file.flush
|
|
99
|
+
yield file.path
|
|
100
|
+
ensure
|
|
101
|
+
file&.close
|
|
102
|
+
file&.unlink
|
|
103
|
+
end
|
|
70
104
|
end
|
|
71
105
|
|
|
72
106
|
# One key (password) associated with the repository, from {Yobi::Repository#key_list}.
|
|
73
|
-
class Key <
|
|
107
|
+
class Key < Yobi::FancyHash
|
|
74
108
|
# @return [String]
|
|
75
109
|
def id
|
|
76
110
|
self["id"]
|
|
@@ -96,31 +130,4 @@ module Yobi
|
|
|
96
130
|
@created ||= Time.parse(self["created"])
|
|
97
131
|
end
|
|
98
132
|
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
133
|
end
|