yobi 0.1.0 → 0.3.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.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "json"
5
4
 
6
5
  module Yobi
7
6
  # One Restic repository. Every Restic subcommand that operates on a
@@ -9,13 +8,13 @@ module Yobi
9
8
  class Repository
10
9
  # @return [String] the repository location
11
10
  attr_reader :url
12
- # @return [String, Array, Symbol, #call, nil] the repository's encryption password, as given to {#initialize}
11
+ # @return [String, Array, Symbol, #call] the repository's encryption password, as given to {#initialize}
13
12
  attr_reader :password
14
13
  # @return [Hash, #call] the storage backend's own credentials, as given to {#initialize}
15
14
  attr_reader :backend_credentials
16
15
 
17
16
  # @param url [String] the repository location, e.g. `"s3:s3.amazonaws.com/bucket"`
18
- # @param password [String, Array, Symbol, #call, nil] a literal password; a
17
+ # @param password [String, Array, Symbol, #call] a literal password; a
19
18
  # `[:command, "..."]`/`[:file, "..."]` tuple, resolved natively by Restic
20
19
  # itself; `:insecure_no_password`; or anything responding to `#call`
21
20
  # (invoked fresh immediately before every Restic invocation)
@@ -24,7 +23,11 @@ module Yobi
24
23
  # responding to `#call` returning such a Hash
25
24
  # @param restic [Yobi::Restic, String, nil] a `Restic` instance to share,
26
25
  # a bare Restic binary path, or `nil` to create a default one
27
- def initialize(url:, password: nil, backend_credentials: {}, restic: nil)
26
+ # @raise [ArgumentError] if `password:` is `nil`, or if `password:`/`backend_credentials:`/`restic:` is an invalid shape
27
+ def initialize(url:, password:, backend_credentials: {}, restic: nil)
28
+ validate_password_shape!(password)
29
+ validate_backend_credentials_shape!(backend_credentials)
30
+
28
31
  @url, @extracted_rest_credentials = extract_rest_credentials(url)
29
32
  @password = password
30
33
  @backend_credentials = backend_credentials
@@ -43,47 +46,10 @@ module Yobi
43
46
  "#<#{self.class} url=#{url.inspect} password=#{redacted_password.inspect} backend_credentials=#{redacted_backend_credentials.inspect}>"
44
47
  end
45
48
 
46
- # `restic init`: creates the repository at {#url}.
47
- #
48
- # @param copy_chunker_params [Boolean] copy chunker parameters from `from_repo:`/`from_repository_file:`
49
- # @param from_insecure_no_password [Boolean]
50
- # @param from_key_hint [String, nil]
51
- # @param from_password_command [String, nil]
52
- # @param from_password_file [String, nil]
53
- # @param from_repo [String, nil]
54
- # @param from_repository_file [String, nil]
55
- # @param repository_version [String, nil]
56
- # @return [Hash] Restic's own `"initialized"` message
57
- def init(copy_chunker_params: false, from_insecure_no_password: false, from_key_hint: nil,
58
- from_password_command: nil, from_password_file: nil, from_repo: nil, from_repository_file: nil,
59
- repository_version: nil)
60
- argv = build_argv("init") do |a|
61
- a.flag(:copy_chunker_params) if copy_chunker_params
62
- a.flag(:from_insecure_no_password) if from_insecure_no_password
63
- a.flag(:from_key_hint, from_key_hint) unless from_key_hint.nil?
64
- a.flag(:from_password_command, from_password_command) unless from_password_command.nil?
65
- a.flag(:from_password_file, from_password_file) unless from_password_file.nil?
66
- a.flag(:from_repo, from_repo) unless from_repo.nil?
67
- a.flag(:from_repository_file, from_repository_file) unless from_repository_file.nil?
68
- a.flag(:repository_version, repository_version) unless repository_version.nil?
69
- end
70
- execution = run_restic(argv)
71
- JSON.parse(execution[:output].to_s)
72
- end
73
-
74
- # `restic cat config`: the repository's own config document.
75
- #
76
- # @return [Hash]
77
- # @raise [Yobi::RepositoryNotFound, Yobi::AuthenticationFailed]
78
- def config
79
- execution = run_restic(build_argv("cat", "config"))
80
- JSON.parse(execution[:output].to_s)
81
- end
82
-
83
49
  private
84
50
 
85
- def run_restic(argv, extra_env: {}, &block)
86
- @restic.run(argv, extra_env: env.merge(extra_env), &block)
51
+ def run_restic(argv, extra_env: {}, output: nil, &block)
52
+ @restic.run(argv, extra_env: env.merge(extra_env), output: output, &block)
87
53
  end
88
54
 
89
55
  def build_argv(*base)
@@ -154,6 +120,51 @@ module Yobi
154
120
  end
155
121
  end
156
122
 
123
+ def from_repo_password(from_repo)
124
+ from_repo.password if from_repo.is_a?(Repository)
125
+ end
126
+
127
+ def resolved_from_repo_flag(from_repo)
128
+ case from_repo
129
+ in Repository
130
+ [:from_repo, from_repo.url]
131
+ in String
132
+ [:from_repo, from_repo]
133
+ in [:file, String => path]
134
+ [:from_repository_file, path]
135
+ else
136
+ raise ArgumentError, "invalid from_repo shape"
137
+ end
138
+ end
139
+
140
+ def validate_password_shape!(value, name: "password")
141
+ case value
142
+ in nil
143
+ raise ArgumentError, "#{name}: is required (use :insecure_no_password if there truly is none)"
144
+ in :insecure_no_password | String | [:command, String] | [:file, String]
145
+ true
146
+ in callable if callable.respond_to?(:call)
147
+ true
148
+ else
149
+ raise ArgumentError, "invalid #{name} shape"
150
+ end
151
+ end
152
+
153
+ def validate_from_password_shape!(value)
154
+ validate_password_shape!(value, name: "from_password")
155
+ end
156
+
157
+ def validate_backend_credentials_shape!(value)
158
+ case value
159
+ in Hash
160
+ true
161
+ in callable if callable.respond_to?(:call)
162
+ true
163
+ else
164
+ raise ArgumentError, "invalid backend_credentials shape"
165
+ end
166
+ end
167
+
157
168
  def extract_rest_credentials(url)
158
169
  return [url, {}] unless url.start_with?("rest:")
159
170
 
@@ -174,6 +185,8 @@ module Yobi
174
185
  restic_or_restic_path
175
186
  in String | nil
176
187
  Yobi::Restic.new(restic_or_restic_path)
188
+ else
189
+ raise ArgumentError, "invalid restic shape"
177
190
  end
178
191
  end
179
192
  end
data/lib/yobi/restic.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tempfile"
4
3
  require "open3"
5
4
  require "json"
6
5
 
@@ -164,10 +163,10 @@ module Yobi
164
163
 
165
164
  # `restic version`: the installed binary's own version info.
166
165
  #
167
- # @return [Hash] `"version"`, `"go_version"`, `"go_os"`, `"go_arch"`
166
+ # @return [Yobi::ResticVersion]
168
167
  def version
169
168
  execution = run(build_argv("version"), skip_version_check: true)
170
- JSON.parse(execution[:output].to_s)
169
+ Yobi::ResticVersion.new(parse_version_output(execution[:output].to_s))
171
170
  end
172
171
 
173
172
  # `restic cache`: lists and optionally cleans local cache directories.
@@ -193,13 +192,16 @@ module Yobi
193
192
  # @param argv [Array<String>]
194
193
  # @param extra_env [Hash{String => String}]
195
194
  # @param skip_version_check [Boolean] used internally by {#version} to avoid recursing into {#ensure_minimum_version!}
196
- # @yieldparam message [Hash] a parsed JSON line, as the command runs
195
+ # @param output [Yobi::ResticOutput, nil] a caller-configured one (e.g. with its own `transform:`),
196
+ # used instead of a plain one this creates itself; forces streaming even without a block
197
+ # @yieldparam message [Object] each message, live, as the command runs - transformed via
198
+ # `output`'s own `transform:` if it has one, the raw parsed Hash otherwise
197
199
  # @return [Hash] `{exit_code:, output:, argv:}` on exit code 0/3
198
200
  # @raise [Yobi::RepositoryNotFound, Yobi::RepositoryLocked, Yobi::AuthenticationFailed, Yobi::ResticCommandFailed]
199
- def run(argv, extra_env: {}, skip_version_check: false, &block)
201
+ def run(argv, extra_env: {}, skip_version_check: false, output: nil, &block)
200
202
  ensure_minimum_version! unless skip_version_check
201
- execution = if block
202
- execute_with_streaming(argv, extra_env, &block)
203
+ execution = if output || block
204
+ execute_with_streaming(argv, extra_env, output: output, &block)
203
205
  else
204
206
  execute(argv, extra_env)
205
207
  end
@@ -241,15 +243,14 @@ module Yobi
241
243
  # @return [Object] the block's own return value, otherwise
242
244
  def run_dump(argv, extra_env: {})
243
245
  ensure_minimum_version!
244
- file = Tempfile.new("yobi-restic-output")
245
- file.unlink
246
+ output = Yobi::ResticOutput.new
246
247
  read_end, write_end = IO.pipe
247
248
  read_end.binmode
248
249
 
249
- pid = Process.spawn(env.merge(extra_env), restic_path, *argv, out: write_end, err: file)
250
+ pid = Process.spawn(env.merge(extra_env), restic_path, *argv, out: write_end, err: output.file)
250
251
  write_end.close
251
252
 
252
- return Yobi::IOHandle.new(read_end, pid: pid, output_file: file, argv: argv) unless block_given?
253
+ return Yobi::IOHandle.new(read_end, pid: pid, output: output, argv: argv) unless block_given?
253
254
 
254
255
  begin
255
256
  result = yield read_end
@@ -258,10 +259,10 @@ module Yobi
258
259
  _, status = Process.wait2(pid)
259
260
  end
260
261
 
261
- self.class.dispatch(exit_code: status.exitstatus, output: Yobi::ResticOutput.new(file), argv: argv)
262
+ self.class.dispatch(exit_code: status.exitstatus, output: output, argv: argv)
262
263
  result
263
264
  rescue Errno::ENOENT
264
- file&.close
265
+ output.file.close
265
266
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
266
267
  end
267
268
 
@@ -284,6 +285,20 @@ module Yobi
284
285
 
285
286
  private
286
287
 
288
+ # An old enough Restic ignores --json for `version` entirely and
289
+ # prints plain text instead (e.g. "restic 0.9.6 compiled with
290
+ # go1.13.4 on linux/amd64"), which JSON.parse can't handle.
291
+ VERSION_LINE_PATTERN = /restic\s+(\d+\.\d+\.\d+\S*)/
292
+
293
+ def parse_version_output(raw)
294
+ JSON.parse(raw)
295
+ rescue JSON::ParserError
296
+ match = VERSION_LINE_PATTERN.match(raw)
297
+ raise unless match
298
+
299
+ {"version" => match[1]}
300
+ end
301
+
287
302
  def build_argv(*base)
288
303
  builder = ArgvBuilder.new
289
304
  builder.append(*base)
@@ -304,38 +319,64 @@ module Yobi
304
319
  end
305
320
 
306
321
  def execute(argv, extra_env)
307
- file = Tempfile.new("yobi-restic-output")
308
- file.unlink
309
- pid = Process.spawn(env.merge(extra_env), restic_path, *argv, out: file, err: file)
322
+ output = Yobi::ResticOutput.new
323
+ pid = Process.spawn(env.merge(extra_env), restic_path, *argv, out: output.file, err: output.file)
310
324
  _, status = Process.wait2(pid)
311
- {exit_code: status.exitstatus, output: Yobi::ResticOutput.new(file), argv: argv}
325
+ {exit_code: status.exitstatus, output: output, argv: argv}
312
326
  rescue Errno::ENOENT
313
- file&.close
327
+ output.file.close
314
328
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
315
329
  end
316
330
 
317
- def execute_with_streaming(argv, extra_env)
318
- file = Tempfile.new("yobi-restic-output")
319
- file.unlink
331
+ def execute_with_streaming(argv, extra_env, output: nil)
332
+ output ||= Yobi::ResticOutput.new
320
333
 
321
- Open3.popen2e(env.merge(extra_env), restic_path, *argv) do |stdin, merged_output, wait_thr|
334
+ Open3.popen3(env.merge(extra_env), restic_path, *argv) do |stdin, stdout, stderr, wait_thr|
322
335
  stdin.close
323
- merged_output.each_line do |line|
324
- file.write(line)
325
- parsed = parse_streamed_line(line)
326
- yield parsed if parsed
336
+ readers = {stdout => :stdout, stderr => :stderr}
337
+
338
+ until readers.empty?
339
+ ready, = IO.select(readers.keys)
340
+ ready.each do |io|
341
+ line = io.gets
342
+ if line.nil?
343
+ readers.delete(io)
344
+ next
345
+ end
346
+
347
+ parsed = output.write_and_parse(line, readers[io])
348
+ yield parsed if parsed && block_given?
349
+ end
327
350
  end
328
- {exit_code: wait_thr.value.exitstatus, output: Yobi::ResticOutput.new(file), argv: argv}
351
+
352
+ {exit_code: wait_thr.value.exitstatus, output: output, argv: argv}
329
353
  end
330
354
  rescue Errno::ENOENT
331
- file&.close
355
+ output.file.close
332
356
  raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
333
357
  end
358
+ end
359
+
360
+ # The result of one {Yobi::Restic#version} call.
361
+ class ResticVersion < Yobi::FancyHash
362
+ # @return [String]
363
+ def version
364
+ self["version"]
365
+ end
334
366
 
335
- def parse_streamed_line(line)
336
- JSON.parse(line)
337
- rescue JSON::ParserError
338
- nil
367
+ # @return [String, nil]
368
+ def go_version
369
+ self["go_version"]
370
+ end
371
+
372
+ # @return [String, nil]
373
+ def go_os
374
+ self["go_os"]
375
+ end
376
+
377
+ # @return [String, nil]
378
+ def go_arch
379
+ self["go_arch"]
339
380
  end
340
381
  end
341
382
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Yobi
4
- # Captures a line's message_type without fully parsing it as JSON.
5
- #
6
- # @private
7
- MESSAGE_TYPE_LINE_PATTERN = /"message_type"\s*:\s*"(\w+)"/
3
+ require "json"
4
+ require "tempfile"
8
5
 
6
+ module Yobi
9
7
  # Wraps the tempfile a repository operation's stdout+stderr are both
10
8
  # written to, with random access so a caller asking only for the last
11
9
  # line (see {#last_line}) doesn't have to read the entire output.
@@ -15,11 +13,19 @@ module Yobi
15
13
  # Default read window size, in bytes, for {#last_line}.
16
14
  DEFAULT_TAIL_CHUNK_SIZE = 4096
17
15
 
18
- # @param file [File]
16
+ # @return [File] a fresh, already-unlinked tempfile of its own
17
+ attr_reader :file
18
+
19
19
  # @param tail_chunk_size [Integer] read window size for {#last_line}
20
- def initialize(file, tail_chunk_size: DEFAULT_TAIL_CHUNK_SIZE)
21
- @file = file
20
+ # @param transform [Proc, nil] used by {#write_and_parse} during a streaming run to transform
21
+ # each message, and as {#messages}' default transform afterward, when no explicit one is given
22
+ def initialize(tail_chunk_size: DEFAULT_TAIL_CHUNK_SIZE, transform: nil)
23
+ @file = Tempfile.new("yobi-restic-output")
24
+ @file.unlink
22
25
  @tail_chunk_size = tail_chunk_size
26
+ @transform = transform
27
+ @index = nil
28
+ @stderr_offsets = []
23
29
  end
24
30
 
25
31
  # Byte offsets of every line, grouped by message_type. Built once and
@@ -69,26 +75,177 @@ module Yobi
69
75
  @file.each_line(&block)
70
76
  end
71
77
 
78
+ # Every message recorded in {#index}, restricted to one +message_type+
79
+ # if given, in file order. Without an explicit transform block, uses
80
+ # the +transform:+ given to {#initialize} (if any); with neither, just
81
+ # yields/returns the raw parsed Hash. Whenever a transform (explicit
82
+ # or the stored one) applies, wraps the result in a {LazyList}
83
+ # instead of yielding.
84
+ #
85
+ # @param message_type [String, nil] every message, if omitted
86
+ # @yieldparam message [Hash] the raw parsed message, to transform
87
+ # @return [Yobi::ResticOutput::LazyList] if a transform block (or a stored transform) applies
88
+ # @return [Enumerator] of raw Hashes, otherwise
89
+ def messages(message_type = nil, &transform)
90
+ transform ||= @transform
91
+ return raw_messages(message_type) unless transform
92
+
93
+ offsets = message_type ? index[message_type] : index.values.flatten.sort
94
+ LazyList.new(self, offsets, transform)
95
+ end
96
+
97
+ # Every line known to have arrived on stderr rather than stdout during
98
+ # a streaming run (see {#write_and_parse}), in file order.
99
+ #
100
+ # @yieldparam line [String]
101
+ # @return [Enumerator] if no block is given
102
+ def stderr_lines
103
+ return enum_for(:stderr_lines) unless block_given?
104
+
105
+ @stderr_offsets.each { |offset| yield read_line_at(offset) }
106
+ end
107
+
108
+ # Called once per line read from a live streaming run's stdout/stderr.
109
+ # Writes it to the file, parses it, and - if it's a JSON message -
110
+ # indexes it by message_type and returns it transformed via the
111
+ # +transform:+ given to {#initialize} (or the raw parsed Hash, if none
112
+ # was given). A non-JSON line's offset is recorded in {#stderr_lines}
113
+ # instead, if +origin+ is +:stderr+; either way, returns nil.
114
+ #
115
+ # @param line [String]
116
+ # @param origin [:stdout, :stderr]
117
+ # @return [Object, nil]
118
+ def write_and_parse(line, origin)
119
+ offset = @file.pos
120
+ @file.write(line)
121
+
122
+ parsed = begin
123
+ JSON.parse(line)
124
+ rescue JSON::ParserError
125
+ nil
126
+ end
127
+
128
+ if parsed
129
+ (@index ||= Hash.new { |hash, message_type| hash[message_type] = [] })[parsed["message_type"]] << offset if parsed["message_type"]
130
+ @transform ? @transform.call(parsed) : parsed
131
+ elsif origin == :stderr
132
+ @stderr_offsets << offset
133
+ nil
134
+ end
135
+ end
136
+
72
137
  # @return [String] the full captured output
73
138
  def to_s
74
139
  @file.rewind
75
140
  @file.read
76
141
  end
77
142
 
143
+ # @return [String]
144
+ def inspect
145
+ "#<#{self.class} #{@file.size} bytes>"
146
+ end
147
+
78
148
  private
79
149
 
80
- def build_index
81
- index = Hash.new { |hash, message_type| hash[message_type] = [] }
150
+ def raw_messages(message_type)
151
+ return enum_for(:raw_messages, message_type) unless block_given?
152
+
153
+ if message_type
154
+ index[message_type].each { |offset| yield JSON.parse(read_line_at(offset)) }
155
+ else
156
+ each_parsed_line { |_offset, parsed| yield parsed }
157
+ end
158
+ end
159
+
160
+ def each_parsed_line
82
161
  @file.rewind
83
162
  loop do
84
163
  offset = @file.pos
85
164
  line = @file.gets
86
165
  break unless line
87
166
 
88
- match = MESSAGE_TYPE_LINE_PATTERN.match(line)
89
- index[match[1]] << offset if match
167
+ parsed = begin
168
+ JSON.parse(line)
169
+ rescue JSON::ParserError
170
+ nil
171
+ end
172
+ yield(offset, parsed) if parsed && parsed["message_type"]
90
173
  end
174
+ end
175
+
176
+ def build_index
177
+ index = Hash.new { |hash, message_type| hash[message_type] = [] }
178
+ each_parsed_line { |offset, parsed| index[parsed["message_type"]] << offset }
91
179
  index
92
180
  end
181
+
182
+ # A lazily-transformed view over a set of {#index} offsets, printing
183
+ # like a plain Array (bounded to a preview, unlike one). Returned by
184
+ # {ResticOutput#messages} when given a transform block.
185
+ # #inspect/#pretty_print's preview is memoized, and #each reuses it
186
+ # rather than re-reading/re-transforming those same offsets again
187
+ # when the caller iterates the whole thing.
188
+ #
189
+ # @private
190
+ class LazyList
191
+ include Enumerable
192
+
193
+ # Items #inspect/#pretty_print show before truncating with "...".
194
+ # One louder than ActiveRecord::Relation#inspect's own preview size.
195
+ INSPECT_PREVIEW_SIZE = 11
196
+
197
+ # @param output [Yobi::ResticOutput]
198
+ # @param offsets [Array<Integer>]
199
+ # @param transform [Proc] applied to each offset's raw parsed Hash
200
+ def initialize(output, offsets, transform)
201
+ @output = output
202
+ @offsets = offsets
203
+ @transform = transform
204
+ end
205
+
206
+ # @yieldparam item [Object]
207
+ # @return [Enumerator] if no block is given
208
+ def each
209
+ return enum_for(:each) unless block_given?
210
+
211
+ preview.each { |item| yield item }
212
+ @offsets[preview.size..].each { |offset| yield transform_at(offset) }
213
+ end
214
+
215
+ # The number of messages, known upfront from {#index} - unlike
216
+ # +Enumerable#count+, doesn't read/transform a single one to answer.
217
+ #
218
+ # @return [Integer]
219
+ def size
220
+ @offsets.size
221
+ end
222
+ alias_method :length, :size
223
+
224
+ # @return [String]
225
+ def inspect
226
+ shown = preview.first(INSPECT_PREVIEW_SIZE).map(&:inspect).join(", ")
227
+ shown += ", ..." if preview.size > INSPECT_PREVIEW_SIZE
228
+ "[#{shown}]"
229
+ end
230
+
231
+ # @return [void]
232
+ def pretty_print(q)
233
+ q.group(1, "[", "]") do
234
+ q.seplist(preview.first(INSPECT_PREVIEW_SIZE)) { |item| q.pp item }
235
+ q.text ", ..." if preview.size > INSPECT_PREVIEW_SIZE
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ # @return [Array<Object>]
242
+ def preview
243
+ @preview ||= @offsets.first(INSPECT_PREVIEW_SIZE + 1).map { |offset| transform_at(offset) }
244
+ end
245
+
246
+ def transform_at(offset)
247
+ @transform.call(JSON.parse(@output.read_line_at(offset)))
248
+ end
249
+ end
93
250
  end
94
251
  end
data/lib/yobi/snapshot.rb CHANGED
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "time"
4
- require "delegate"
5
4
 
6
5
  module Yobi
7
6
  # One snapshot, as Restic reports it. Shared across {Yobi::Repository#snapshots},
8
7
  # {Yobi::Repository#ls}, and {Yobi::Repository#forget}'s keep/remove entries.
9
- class Snapshot < SimpleDelegator
8
+ class Snapshot < Yobi::FancyHash
10
9
  # @return [String]
11
10
  def id
12
11
  self["id"]
@@ -41,5 +40,85 @@ module Yobi
41
40
  def parent_id
42
41
  self["parent"]
43
42
  end
43
+
44
+ # @return [Yobi::SnapshotSummary]
45
+ def summary
46
+ @summary ||= SnapshotSummary.new(self["summary"] || {})
47
+ end
48
+ end
49
+
50
+ # The `"summary"` field of a {Yobi::Snapshot}, the stats recorded when it
51
+ # was created.
52
+ # https://restic.readthedocs.io/en/stable/075_scripting.html#snapshotsummary-object
53
+ class SnapshotSummary < Yobi::FancyHash
54
+ # @return [Time, nil]
55
+ def backup_start
56
+ @backup_start ||= Time.parse(self["backup_start"]) if self["backup_start"]
57
+ end
58
+
59
+ # @return [Time, nil]
60
+ def backup_end
61
+ @backup_end ||= Time.parse(self["backup_end"]) if self["backup_end"]
62
+ end
63
+
64
+ # @return [Integer]
65
+ def files_new
66
+ self["files_new"] || 0
67
+ end
68
+
69
+ # @return [Integer]
70
+ def files_changed
71
+ self["files_changed"] || 0
72
+ end
73
+
74
+ # @return [Integer]
75
+ def files_unmodified
76
+ self["files_unmodified"] || 0
77
+ end
78
+
79
+ # @return [Integer]
80
+ def dirs_new
81
+ self["dirs_new"] || 0
82
+ end
83
+
84
+ # @return [Integer]
85
+ def dirs_changed
86
+ self["dirs_changed"] || 0
87
+ end
88
+
89
+ # @return [Integer]
90
+ def dirs_unmodified
91
+ self["dirs_unmodified"] || 0
92
+ end
93
+
94
+ # @return [Integer]
95
+ def data_blobs
96
+ self["data_blobs"] || 0
97
+ end
98
+
99
+ # @return [Integer]
100
+ def tree_blobs
101
+ self["tree_blobs"] || 0
102
+ end
103
+
104
+ # @return [Integer]
105
+ def data_added
106
+ self["data_added"] || 0
107
+ end
108
+
109
+ # @return [Integer]
110
+ def data_added_packed
111
+ self["data_added_packed"] || 0
112
+ end
113
+
114
+ # @return [Integer]
115
+ def total_files_processed
116
+ self["total_files_processed"] || 0
117
+ end
118
+
119
+ # @return [Integer]
120
+ def total_bytes_processed
121
+ self["total_bytes_processed"] || 0
122
+ end
44
123
  end
45
124
  end
data/lib/yobi/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A Ruby library for the Restic backup tool, wrapping the `restic` CLI in
3
+ # A Ruby library for the Restic backup program, wrapping the `restic` CLI in
4
4
  # plain Ruby objects instead of shelling out to flags and raw JSON by hand.
5
5
  module Yobi
6
6
  # @return [String]
7
- VERSION = "0.1.0"
7
+ VERSION = "0.3.0"
8
8
  end
data/lib/yobi.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "yobi/version"
4
+ require_relative "yobi/fancy_hash"
4
5
  require_relative "yobi/errors"
5
6
  require_relative "yobi/argv_builder"
6
7
  require_relative "yobi/restic_output"
7
8
  require_relative "yobi/io_handle"
8
- require_relative "yobi/mount"
9
+ require_relative "yobi/mount_handle"
9
10
  require_relative "yobi/restic"
10
11
  require_relative "yobi/snapshot"
11
12
  require_relative "yobi/repository"