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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d127920e9ec0a4e254c53ce4b1a835e9ee4d94299fc93f7880debc55a5ec7472
4
- data.tar.gz: 80415f3dfde8205cd62e7079c71006a6b6e599b2c3074ca1cc873c091a0c1229
3
+ metadata.gz: edd02fb1bbb985e22d5e32e8558b37f469809e9b5cdc079c6b4885e5ada5a775
4
+ data.tar.gz: 4d6a90b324596e6fc375a1338fdba1c70d84a6bf9504b15513614c87458bd7c0
5
5
  SHA512:
6
- metadata.gz: 11134300c532ef9777e1a9a10e60b4a7d8cca05223e8c9ad9c0917bc4118bb5c609bb44c7037bd2c01615873884bd4a9ca02ad7f6fe44c00e536bcff4a2f7b0d
7
- data.tar.gz: c52bc48ba265f031f1d37ddf153decec2ee9e6ab07de1a2501d0f5860ffe9b61a877d8b1c9b8553a5f2c612803b2cf188ced9833be11b8de00e22a043a00d9ca
6
+ metadata.gz: 9c11e446c74d42a68b7a34c61b4952451f0929d56114a6439ec6462f854019ea40d7844dd730371026a679f744a464600c997ce76b884de538a38ee879348efc
7
+ data.tar.gz: e3245e59a13c6b9f8712824406f0844106e1495a543ff68cf2ac11210835ef480109755acf2019ca5b10708d46436bc1e58f7e9e122f450ce3dd4f40395695fb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.2.0] - 2026-08-03
2
+
3
+ ### Added
4
+
5
+ - `Repository#init_mirror` sets up a new destination repository, pre-initialized with chunker parameters copied from the source, so a later `#copy` between the two can deduplicate.
6
+ - `Repository#cat_config` aliased as `#config`.
7
+ - `CheckSummary` gained `#suggest_repair_index?` and `#suggest_prune?`.
8
+ - `Restic#version` now tolerates a too-old restic binary that ignores `--json` for the `version` subcommand, falling back to parsing its plain-text output instead of raising a JSON parse error.
9
+
10
+ ### Changed
11
+
12
+ - **Breaking:** `Repository#initialize`'s `password:` is now required - passing `nil` raises `ArgumentError`. Yobi no longer falls back to an ambient `RESTIC_PASSWORD`/`RESTIC_PASSWORD_COMMAND` env var on its own; pass `:insecure_no_password` explicitly if the repository truly has none. `Repository#init_mirror`'s `password:` is likewise now required.
13
+ - **Breaking:** `#key_add`/`#key_passwd`'s new-password kwargs and `#init`'s `from_password:` collapsed to one polymorphic kwarg each, accepting the same literal/`[:command, ...]`/`[:file, ...]`/callable shapes as `password:`, instead of separate shape-specific kwargs.
14
+ - **Breaking:** reworked how `#backup`/`#restore`/`#check`/`#diff`/`#tag`/`#forget`/`#ls` report results. Outcome objects (`BackupOutcome`, `RestoreOutcome`, `CheckOutcome`, `DiffOutcome`, `TagOutcome`, `LsOutcome`) are now plain classes wrapping a `ResticOutput` rather than `Struct.new(:exit_code, :output)`; per-message classes (`BackupError`, `BackupStatus`, `CheckError`, `DiffChange`, `TagChange`, and friends) are built on the new `Yobi::FancyHash` rather than `SimpleDelegator`; `#summary` (aliased `#report`) now returns a typed summary object (`BackupSummary`, `CheckSummary`, `TagSummary`) instead of a raw Hash; per-item accessors (`#errors`, `#changes`, `#entries`) are now lazy Enumerables instead of eagerly-built Arrays.
15
+ - **Breaking:** renamed `Yobi::LsEntry` to `Yobi::Node`, `Yobi::ForgetReason` to `Yobi::KeepReason`, and `Yobi::Mount` to `Yobi::MountHandle`.
16
+
1
17
  ## [0.1.0] - 2026-07-29
2
18
 
3
19
  Initial release.
data/README.md CHANGED
@@ -4,7 +4,7 @@ Yobi is a Ruby interface for the [Restic](https://restic.net/) backup program.
4
4
 
5
5
  ## Installation
6
6
 
7
- Install the gem:
7
+ ### Install the gem
8
8
 
9
9
  ```bash
10
10
  bundle add yobi
@@ -16,25 +16,11 @@ or, without Bundler:
16
16
  gem install yobi
17
17
  ```
18
18
 
19
- ### Installing Restic
19
+ ### Install Restic
20
20
 
21
21
  Yobi shells out to a real `restic` binary; it doesn't install or bundle one. You need `restic` on `PATH` (or point Yobi at it explicitly, see below) before any of this works.
22
22
 
23
- Install it however you'd install any other system tool:
24
-
25
- ```bash
26
- # macOS
27
- brew install restic
28
-
29
- # Debian/Ubuntu
30
- apt install restic
31
-
32
- # Arch
33
- pacman -S restic
34
-
35
- # or download a binary directly
36
- # https://github.com/restic/restic/releases
37
- ```
23
+ See [Restic's installation guide](https://restic.readthedocs.io/en/stable/020_installation.html) for how to install it on your platform.
38
24
 
39
25
  Confirm it's reachable:
40
26
 
@@ -103,9 +89,9 @@ outcome = repo.backup(
103
89
  excludes: ["*.tmp", "node_modules"],
104
90
  tags: ["documents", "daily"]
105
91
  )
106
- outcome.success? # => true
107
- outcome.report["snapshot_id"]
108
- outcome.report["data_added"]
92
+ outcome.errors.none? # => true
93
+ outcome.summary["snapshot_id"]
94
+ outcome.summary["data_added"]
109
95
 
110
96
  repo.snapshots.each do |snapshot|
111
97
  puts "#{snapshot.short_id} #{snapshot.time} #{snapshot.paths.join(", ")}"
@@ -114,7 +100,7 @@ end
114
100
  repo.restore(snapshot_id: "latest", target: "/tmp/restore")
115
101
  ```
116
102
 
117
- Every verb method returns something typed rather than a raw Hash. `#backup` returns a `BackupOutcome` (`#success?`, `#report`, `#errors`), `#snapshots` returns an Enumerable of `Snapshot` objects, and so on throughout the API.
103
+ Most verb methods return something typed rather than a raw Hash. `#backup` returns a `BackupOutcome` (`#summary`, `#errors`), `#snapshots` returns an Enumerable of `Snapshot` objects, and so on throughout the API - the low-level `cat_*` introspection commands are the exception, returning Restic's own raw JSON directly (see ["Low-level (`cat`) and object listing"](#low-level-cat-and-object-listing) below).
118
104
 
119
105
  ## Constructing a `Repository`
120
106
 
@@ -136,7 +122,7 @@ See [Restic's documentation](https://restic.readthedocs.io/en/stable/030_prepari
136
122
 
137
123
  ### `password:`
138
124
 
139
- Identifies the repository's encryption password. Five shapes:
125
+ Identifies the repository's encryption password. Required - pass one of five shapes:
140
126
 
141
127
  ```ruby
142
128
  # A literal password
@@ -159,8 +145,6 @@ password = :insecure_no_password
159
145
  Yobi::Repository.new(url: "...", password: password)
160
146
  ```
161
147
 
162
- If `password:` is left unspecified, Yobi adds nothing extra to the environment: Restic falls back to `RESTIC_PASSWORD`/`RESTIC_PASSWORD_COMMAND` if either happens to already be set in the calling process's own environment (a container, systemd unit, or parent shell outside Yobi's control). If nothing is set anywhere, Restic refuses a truly empty password (unless `--insecure-no-password` is given) and fails loudly and safely on its own.
163
-
164
148
  ### `backend_credentials:`
165
149
 
166
150
  Separate from `password:`: this is whatever the storage *backend* needs to authenticate, unrelated to whether the repository's contents can be decrypted. A Hash of env vars, or a callable returning one, resolved fresh on every call the same way a callable `password:` is:
@@ -243,17 +227,17 @@ Creates the repository at `url`.
243
227
 
244
228
  ```ruby
245
229
  repo.init
246
- # => {"message_type"=>"initialized", "id"=>"...", "repository"=>"..."}
230
+ # => #<Yobi::Initialized id="..." repository="...">
247
231
  ```
248
232
 
249
- `copy_chunker_params:`/`from_repo:`/`from_repository_file:`/`from_key_hint:`/`from_password_command:`/`from_password_file:`/`from_insecure_no_password:` copy chunker parameters from a secondary repository. Useful when you plan to `#copy` between the two later, since chunker params have to match for deduplication to work across repositories. `repository_version:` picks a specific repository format version instead of Restic's current default.
233
+ `copy_chunker_params:` copies chunker parameters from another repository (`from_repo:`/`from_password:`/etc.), so a later `#copy` between the two can deduplicate - see `#init_mirror` under ["Across repositories"](#across-repositories) for a shortcut that sets this up in one call. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#init-instance_method) for the full list of options. Returns an `Initialized` (`#id`/`#repository`).
250
234
 
251
- ### `#config`
235
+ ### `#cat_config`
252
236
 
253
- The repository's own config document (`cat config`), as a Hash. A cheap way to confirm a repository exists and the password is correct without doing anything else.
237
+ The repository's own config document (`cat config`), as a Hash. A cheap way to confirm a repository exists and the password is correct without doing anything else. Aliased `#config`.
254
238
 
255
239
  ```ruby
256
- repo.config
240
+ repo.cat_config
257
241
  # => {"version"=>2, "id"=>"...", "chunker_polynomial"=>"..."}
258
242
  ```
259
243
 
@@ -269,43 +253,22 @@ outcome = repo.backup(
269
253
  excludes: ["*.tmp", "node_modules"],
270
254
  tags: ["documents", "daily"]
271
255
  )
272
- outcome.success? # => true
273
- outcome.report["snapshot_id"]
274
- outcome.report["files_new"]
275
- outcome.report["data_added"]
256
+ outcome.errors.none? # => true
257
+ outcome.summary["snapshot_id"]
258
+ outcome.summary["files_new"]
259
+ outcome.summary["data_added"]
276
260
  ```
277
261
 
278
262
  `source:` is either a path String, or a `[:stdin_from_command, command]`/`[:stdin_from_command, command, filename]` tuple: Restic spawns and executes `command` itself, capturing its stdout as the backup content (see ["Database dumps via `stdin_from_command`"](#database-dumps-via-stdin_from_command) below for a `pg_dump` example). `command` can be a String (tokenized with `Shellwords.split`, so quoted arguments survive) or an Array of already-discrete arguments (used as-is; needed when an argument itself contains a literal space, which `Shellwords` would otherwise split incorrectly).
279
263
 
280
- **Filtering:**
281
-
282
- - `excludes:`/`exclude_files:`/`iexcludes:`/`iexclude_files:` (case-insensitive): exclude by glob pattern or pattern file
283
- - `exclude_if_present:`: skip a directory containing a named marker file
284
- - `exclude_larger_than:`: skip by size
285
- - `exclude_caches:`/`exclude_cloud_files:`: skip CACHEDIR.TAG-marked dirs and cloud-placeholder files respectively
286
- - `files_from:`/`files_from_raw:`/`files_from_verbatim:`: read the backup list from a file instead of (or alongside) `source:`
287
-
288
- **Behavior:**
264
+ `host:` records a hostname on the *new* snapshot - a single value, since this is metadata being written, not a filter, unlike every `hosts:` elsewhere in this API. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#backup-instance_method) for the full list of options (filtering, retention, timing, and more).
289
265
 
290
- - `dry_run:`: report what would happen without doing it
291
- - `force:`: back up unchanged files anyway
292
- - `one_file_system:`: don't cross filesystem boundaries
293
- - `ignore_ctime:`/`ignore_inode:`: relax change detection
294
- - `no_scan:`: skip the pre-backup scan, disabling percentage progress
295
- - `skip_if_unchanged:`: don't create a snapshot if nothing changed
296
- - `parent:`: pick a specific parent snapshot instead of the latest one
297
- - `group_by:`: grouping used to find the parent snapshot instead, e.g. `"host,paths"`, when `parent:` isn't given
298
- - `read_concurrency:`: number of concurrent file reads
299
- - `time:`: record a specific timestamp instead of now
300
- - `with_atime:`: also store files' access times
301
- - `host:`: hostname recorded on the *new* snapshot (a single value: this is metadata being written, not a filter, unlike every `hosts:` elsewhere in this API)
266
+ Returns a `BackupOutcome`: `#summary` (aliased `#report`; a `BackupSummary`, Restic's own summary fields plus `#backup_start`/`#backup_end` parsed into `Time`), `#errors` (lazy Enumerable of `BackupError` - empty unless some files were skipped, e.g. permission errors; a full failure raises before an outcome exists at all, see ["Error handling"](#error-handling) below), `#command_output` (the `source: [:stdin_from_command, ...]` subprocess's own stderr, de-prefixed, if any).
302
267
 
303
- Returns a `BackupOutcome`: `#success?`/`#partial?` (exit code 3, some files were skipped, e.g. permission errors), `#report` (the summary Hash, with `"backup_start"`/`"backup_end"` parsed into `Time`), `#errors` (lazy Enumerable of `BackupError`), `#command_output` (the `source: [:stdin_from_command, ...]` subprocess's own stderr, de-prefixed, if any).
304
-
305
- Also accepts a block, to receive typed progress objects as the backup runs instead of only getting the final `BackupOutcome`:
268
+ Also accepts a block, to receive typed progress objects as the backup runs, on top of (not instead of) the final `BackupOutcome` once it returns. The block also receives a `Yobi::BackupSummary`, the same object `outcome.summary` returns, once the run finishes:
306
269
 
307
270
  ```ruby
308
- repo.backup(source: "/Users/zia/Documents") do |message|
271
+ outcome = repo.backup(source: "/Users/zia/Documents") do |message|
309
272
  case message
310
273
  when Yobi::BackupStatus
311
274
  puts "#{((message.percent_done || 0) * 100).round}% (#{message.files_done}/#{message.total_files})"
@@ -313,8 +276,11 @@ repo.backup(source: "/Users/zia/Documents") do |message|
313
276
  warn "#{message.item}: #{message.message}"
314
277
  when Yobi::BackupVerboseStatus
315
278
  puts "#{message.action} #{message.item}"
279
+ when Yobi::BackupSummary
280
+ puts "done: #{message["data_added"]} bytes added"
316
281
  end
317
282
  end
283
+ outcome.summary["snapshot_id"]
318
284
  ```
319
285
 
320
286
  `Yobi::BackupVerboseStatus` (one message per file) only streams when `verbose: true` is passed; it's not emitted by default, since a large backup can otherwise produce hundreds of MB of these. Behavior without a block is identical, just without the live callbacks; the returned `BackupOutcome` is the same either way.
@@ -325,19 +291,9 @@ end
325
291
  repo.restore(snapshot_id: "latest", target: "/tmp/restore")
326
292
  ```
327
293
 
328
- `snapshot_id:` accepts `"latest"` or a real ID; `target:` is the destination directory.
329
-
330
- - `excludes:`/`includes:` (and their `i`-prefixed case-insensitive/`_file` file-based variants): filter what gets restored. `includes:` has no backup-time equivalent, since restoring is the one direction where "only these" makes sense as well as "all but these."
331
- - `exclude_xattrs:`/`include_xattrs:`: filter extended attributes specifically
332
- - `dry_run:`: report what would happen without doing it
333
- - `delete:`: removes files in `target:` not present in the snapshot
334
- - `overwrite:` (`"always"`/`"if-changed"`/`"if-newer"`): controls what happens to files that already exist there
335
- - `sparse:`: writes sparse files
336
- - `ownership_by_name:`: maps ownership by user/group name instead of numeric ID
337
- - `verify:`: re-reads restored content against the repository to confirm it matches
338
- - `hosts:`/`paths:`/`tags:`: only matter when `snapshot_id:` is `"latest"`, same filters as everywhere else, used to resolve which snapshot "latest" means
294
+ `snapshot_id:` accepts `"latest"` or a real ID; `target:` is the destination directory. `delete:` removes files in `target:` not present in the snapshot - worth knowing about since it's the one option here that can destroy data outside the snapshot itself. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#restore-instance_method) for the full list of options (filtering, `overwrite:` behavior, and more).
339
295
 
340
- Returns a `RestoreOutcome` (`#report`, no `#success?`/`#partial?`: restore has no exit code 3, any item-level failure is a hard error that raises before an outcome exists).
296
+ Returns a `RestoreOutcome`: `#summary` (aliased `#report`; Restic's own summary fields as a plain `Hash`) - restore has no exit code 3, so any item-level failure is a hard error that raises before an outcome exists.
341
297
 
342
298
  Accepts a block the same way `#backup` does, yielding `Yobi::RestoreStatus`/`Yobi::RestoreVerboseStatus` (the latter only when `verbose: true`) instead of `Yobi::BackupStatus`/`Yobi::BackupVerboseStatus`.
343
299
 
@@ -359,7 +315,7 @@ repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.tar")
359
315
  repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.zip", archive: "zip")
360
316
  ```
361
317
 
362
- Give at most one of `target:` or a block. Without either, returns a `Yobi::IOHandle` instead:
318
+ Give at most one of `target:` or a block. Without either, returns a `Yobi::IOHandle` instead. `hosts:`/`paths:`/`tags:` filters are also available when `snapshot_id:` is `"latest"` - see the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#dump-instance_method) for the full list of options.
363
319
 
364
320
  ```ruby
365
321
  handle = repo.dump(snapshot_id: "latest", file: "/var/www")
@@ -375,11 +331,13 @@ end
375
331
 
376
332
  ```ruby
377
333
  outcome = repo.diff(from: older_snapshot.id, to: newer_snapshot.id)
378
- outcome.report["changed_files"]
334
+ outcome.statistics.changed_files
379
335
  outcome.changes.each { |change| puts "#{change.modifier} #{change.path}" }
380
336
  ```
381
337
 
382
- `metadata: true` also reports metadata-only changes (permissions, timestamps) alongside content changes. `change.modifier` is Restic's own single-character code: `+` added, `-` removed, `U` metadata updated, `M` content modified, `T` type changed, `?` bitrot detected.
338
+ `metadata: true` also reports metadata-only changes (permissions, timestamps) alongside content changes. `change.modifier` is Restic's own concatenation of single-character codes: `+` added, `-` removed, `U` metadata updated, `M` content modified, `T` type changed, `?` bitrot detected. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#diff-instance_method) for the full list of options.
339
+
340
+ Returns a `DiffOutcome`: `#statistics` (aliased `#report`; a `DiffStatistics` with `#changed_files` plus `#added`/`#removed` `DiffStat` breakdowns) and lazy `#changes` (Enumerable of `DiffChange`, with predicates like `#modified?`/`#added?` alongside the raw `#modifier`).
383
341
 
384
342
  ## Snapshot management
385
343
 
@@ -391,7 +349,7 @@ repo.snapshots(tags: ["daily"], hosts: "web-1").each do |snapshot|
391
349
  end
392
350
  ```
393
351
 
394
- Returns an Enumerable of `Snapshot` (`#id`, `#short_id`, `#time`, `#host`, `#tags`, `#paths`, `#parent_id`). `compact:` compacts the underlying Restic output; `group_by:`/`latest:` group and limit results, e.g. `latest: 1` per group for "the most recent backup of each host."
352
+ Returns an Enumerable of `Snapshot` (`#id`, `#short_id`, `#time`, `#host`, `#tags`, `#paths`, `#parent_id`, `#summary` - a `SnapshotSummary` with the same stats fields `#backup`'s own summary has). `group_by:`/`latest:` group and limit results, e.g. `latest: 1` per group for "the most recent backup of each host." See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#snapshots-instance_method) for the full list of options.
395
353
 
396
354
  ### `#tag`
397
355
 
@@ -399,7 +357,9 @@ Returns an Enumerable of `Snapshot` (`#id`, `#short_id`, `#time`, `#host`, `#tag
399
357
  repo.tag(snapshot_ids: snapshot.id, add: "verified")
400
358
  ```
401
359
 
402
- `add:`/`remove:`/`set:` (mutually exclusive with `add:`/`remove:` in Restic itself) modify tags on snapshots matched by `snapshot_ids:` (or by `hosts:`/`paths:`/`tags:` filters when no explicit IDs are given). Since tags are part of a snapshot's content-addressed identity, tagging produces a *new* snapshot ID for every snapshot touched. `TagOutcome#changes` gives you the old-ID/new-ID pairs.
360
+ `add:`/`remove:`/`set:` (mutually exclusive with `add:`/`remove:` in Restic itself) modify tags on snapshots matched by `snapshot_ids:` (or by `hosts:`/`paths:`/`tags:` filters when no explicit IDs are given). Since tags are part of a snapshot's content-addressed identity, tagging produces a *new* snapshot ID for every snapshot touched. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#tag-instance_method) for the full list of options.
361
+
362
+ Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#changed_snapshots`) and lazy `#changes` (Enumerable of `TagChange`, the old-ID/new-ID pairs).
403
363
 
404
364
  ### `#forget`
405
365
 
@@ -407,30 +367,34 @@ repo.tag(snapshot_ids: snapshot.id, add: "verified")
407
367
  repo.forget(keep_daily: 7, keep_weekly: 4, keep_monthly: 12, prune: true)
408
368
  ```
409
369
 
410
- Applies a retention policy, removing snapshots that don't match any `keep_*` rule: `keep_last:`/`keep_hourly:`/`keep_daily:`/`keep_weekly:`/`keep_monthly:`/`keep_yearly:` (counts) and `keep_within:`/`keep_within_hourly:`/etc. (durations, e.g. `"30d"`). `keep_tags:` always keeps snapshots carrying any of the given tags regardless of the numeric rules. `prune: true` also reclaims the disk space the forgotten snapshots held (equivalent to a separate `#prune` call afterward); the `max_unused:`/`max_repack_size:`/`repack_*:` kwargs tune that implied prune step exactly like `#prune`'s own. `dry_run:` reports what would be removed without doing it. `unsafe_allow_remove_all:` is required if a policy would remove every snapshot.
370
+ Applies a retention policy, removing snapshots that don't match any `keep_*` rule (counts like `keep_daily:`, or durations like `keep_within:`). `prune: true` also reclaims the disk space the forgotten snapshots held (equivalent to a separate `#prune` call afterward). See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#forget-instance_method) for the full list of options.
411
371
 
412
- Returns an Enumerable of `ForgetGroup` (Restic evaluates the policy per group, by default grouped by host+paths). Each exposes `#keep`/`#remove` (Arrays of `Snapshot`) and `#reasons` (an Array of `ForgetReason` explaining which rule kept each surviving snapshot, e.g. `"daily snapshot"`).
372
+ Returns an Enumerable of `ForgetGroup` (Restic evaluates the policy per group, by default grouped by host+paths). Each exposes `#keep`/`#remove` (Arrays of `Snapshot`) and `#reasons` (an Array of `KeepReason` explaining which rule kept each surviving snapshot, e.g. `"daily snapshot"`).
413
373
 
414
374
  ### `#find`
415
375
 
416
376
  ```ruby
417
377
  repo.find(patterns: "*.pem").each do |matches|
418
- puts "in #{matches.snapshot["short_id"]}: #{matches.hits} hits"
378
+ puts "in #{matches.snapshot_id}: #{matches.hits} hits"
419
379
  matches.matches.each { |m| puts " #{m.path}" }
420
380
  end
421
381
  ```
422
382
 
423
- Searches for files/directories by name pattern across snapshots. `blob:`/`pack:`/`tree:` switch to matching object IDs instead, for low-level troubleshooting. Returns an Enumerable of `MatchesPerSnapshot` (Restic's own docs describe find's output as "organized by snapshot," not a flat list). Each has `#hits` and `#matches` (an Array of `FindMatch`, with the usual file metadata: `#path`, `#size`, `#mtime`, etc.).
383
+ Searches for files/directories by name pattern across snapshots. `blob:`/`pack:`/`tree:` switch to matching object IDs instead, for low-level troubleshooting. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#find-instance_method) for the full list of options.
384
+
385
+ Returns an Enumerable of `MatchesPerSnapshot` (Restic's own docs describe find's output as "organized by snapshot," not a flat list). Each has `#snapshot`/`#snapshot_id` (just the matched snapshot's ID as a String - unlike elsewhere in this API, Restic's own `find` output doesn't include the full record), `#hits`, and `#matches` (an Array of `FindMatch`, with the usual file metadata: `#path`, `#size`, `#mtime`, etc.).
424
386
 
425
387
  ### `#ls`
426
388
 
427
389
  ```ruby
428
390
  outcome = repo.ls(snapshot_id: "latest", dirs: "/var/www")
429
391
  outcome.snapshot.short_id
430
- outcome.entries.each { |entry| puts entry.path }
392
+ outcome.nodes.each { |node| puts node.path }
431
393
  ```
432
394
 
433
- Lists a snapshot's files/directories. `recursive:` descends into subdirectories; `dirs:` restricts to specific paths within the snapshot. Returns an `LsOutcome`: `#snapshot` (the resolved `Snapshot`, useful to see what `"latest"` actually resolved to) and lazy `#entries` (Enumerable of `LsEntry`).
395
+ Lists a snapshot's files/directories. `recursive:` descends into subdirectories. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#ls-instance_method) for the full list of options.
396
+
397
+ Returns an `LsOutcome`: `#snapshot` (the resolved `Snapshot`, useful to see what `"latest"` actually resolved to) and lazy `#nodes` (aliased `#entries`; Enumerable of `Node` - Restic's own term for a file/directory entry).
434
398
 
435
399
  ## Maintenance and health
436
400
 
@@ -438,11 +402,13 @@ Lists a snapshot's files/directories. `recursive:` descends into subdirectories;
438
402
 
439
403
  ```ruby
440
404
  outcome = repo.check(read_data_subset: "5%")
441
- outcome.report["num_errors"]
405
+ outcome.summary.num_errors
442
406
  outcome.errors.each { |error| puts error.message }
443
407
  ```
444
408
 
445
- Verifies repository integrity. `read_data:` also reads and verifies every pack file's actual contents, not just structure (slow, thorough); `read_data_subset:` does a partial version of the same (e.g. `"5%"`, or `"1/20"` for a fifth each day in rotation); `with_cache:` uses the local cache instead of bypassing it. Returns a `CheckOutcome` (no `#success?`/`#partial?`: check has no exit code 3; check `report["num_errors"]` instead).
409
+ Verifies repository integrity. `read_data:` also reads and verifies every pack file's actual contents, not just structure (slow, thorough); `read_data_subset:` does a partial version of the same (e.g. `"5%"`, or `"1/20"` for a fifth each day in rotation). See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#check-instance_method) for the full list of options.
410
+
411
+ Returns a `CheckOutcome`: `#summary` (aliased `#report`; a `CheckSummary` covering the error count and what to run next - repair or prune) and `#errors` (lazy Enumerable of `CheckError`) for problems found.
446
412
 
447
413
  ### `#prune`
448
414
 
@@ -450,7 +416,7 @@ Verifies repository integrity. `read_data:` also reads and verifies every pack f
450
416
  repo.prune(max_unused: "5%")
451
417
  ```
452
418
 
453
- Removes data no longer referenced by any snapshot. `dry_run:` reports what would happen without doing it. `max_unused:` targets a maximum acceptable unused-space ratio after pruning; `max_repack_size:` bounds how much gets repacked in one run (for spreading a large prune across multiple scheduled invocations); `repack_cacheable_only:`/`repack_uncompressed:`/`repack_smaller_than:` tune which packs get rewritten. `unsafe_recover_no_free_space:` proceeds even without enough free space, at the given risk acknowledgement string. Returns `true`.
419
+ Removes data no longer referenced by any snapshot. `dry_run:` reports what would happen without doing it; `max_unused:` targets a maximum acceptable unused-space ratio after pruning. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#prune-instance_method) for the full list of options. Returns `true`.
454
420
 
455
421
  ### `#repair_index`, `#repair_packs`, `#repair_snapshots`
456
422
 
@@ -460,7 +426,7 @@ repo.repair_packs(ids: damaged_pack_ids)
460
426
  repo.repair_snapshots(snapshot_ids: affected_ids, forget: true)
461
427
  ```
462
428
 
463
- `#repair_index` rebuilds the index from the pack files present (the modern successor to Restic's now-deprecated `rebuild-index`). `#repair_packs` extracts intact blobs from damaged pack files and drops the rest. Restic also writes a backup copy of each given pack (named `pack-<id>`) into the *calling process's own current working directory* first, with no flag to disable this; be aware of where your process runs from before calling it. `#repair_snapshots` regenerates snapshots with damaged content removed. This is real data loss for whatever gets removed, so prefer a fresh `#backup` where the source data is still available, and run `#repair_index` first since this depends on a correct index.
429
+ `#repair_index` rebuilds the index from the pack files present (the modern successor to Restic's now-deprecated `rebuild-index`). `#repair_packs` extracts intact blobs from damaged pack files and drops the rest. Restic also writes a backup copy of each given pack (named `pack-<id>`) into the *calling process's own current working directory* first, with no flag to disable this; be aware of where your process runs from before calling it. `#repair_snapshots` regenerates snapshots with damaged content removed. This is real data loss for whatever gets removed, so prefer a fresh `#backup` where the source data is still available, and run `#repair_index` first since this depends on a correct index. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#repair_snapshots-instance_method) for the full list of options.
464
430
 
465
431
  ### `#recover`
466
432
 
@@ -476,7 +442,7 @@ Builds a new snapshot from any data present in the repository but not referenced
476
442
  repo.rewrite(snapshot_ids: old.id, excludes: "*.log", new_time: Time.now.iso8601)
477
443
  ```
478
444
 
479
- Creates new snapshots from existing ones with filters applied or metadata changed. With no `snapshot_ids:`/`hosts:`/`tags:`/`paths:` given, rewrites every snapshot in the repository. Restic's own default, not something Yobi adds. `excludes:`/`exclude_files:`/`includes:`/`include_files:` (and their `i`-prefixed case-insensitive variants) filter file content the same way as `#backup`/`#restore`. `dry_run:` reports what would happen without doing it. `new_host:`/`new_time:` change the recorded hostname/timestamp; `snapshot_summary:` regenerates the snapshot summary. `forget: false` (the default) tags the new snapshots `"rewrite"` and keeps the originals; `forget: true` removes the originals instead (their data isn't reclaimed until a later `#prune`).
445
+ Creates new snapshots from existing ones with filters applied or metadata changed. With no `snapshot_ids:`/`hosts:`/`tags:`/`paths:` given, rewrites every snapshot in the repository - Restic's own default, not something Yobi adds. `dry_run:` reports what would happen without doing it. `forget: false` (the default) tags the new snapshots `"rewrite"` and keeps the originals; `forget: true` removes the originals instead (their data isn't reclaimed until a later `#prune`). See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#rewrite-instance_method) for the full list of options.
480
446
 
481
447
  ### `#migrate`
482
448
 
@@ -498,23 +464,30 @@ Removes stale locks left by other Restic processes. `remove_all: true` removes e
498
464
 
499
465
  ```ruby
500
466
  repo.stats(mode: "raw-data")
501
- # => {"total_size"=>..., "total_file_count"=>..., "snapshots_count"=>...}
467
+ # => #<Yobi::RepositoryStats total_size=... total_file_count=... snapshots_count=...>
502
468
  ```
503
469
 
504
- Repository size/file-count statistics. `mode:` picks the counting strategy, as a String or Symbol: `"restore-size"`/`:restore_size` (default), `"files-by-contents"`/`:files_by_contents`, `"blobs-per-file"`/`:blobs_per_file`, `"raw-data"`/`:raw_data` (actual on-disk size, accounting for deduplication). Anything else raises `ArgumentError`.
470
+ Repository size/file-count statistics. `mode:` picks the counting strategy, as a String or Symbol: `"restore-size"`/`:restore_size` (default), `"files-by-contents"`/`:files_by_contents`, `"blobs-per-file"`/`:blobs_per_file`, `"raw-data"`/`:raw_data` (actual on-disk size, accounting for deduplication). Anything else raises `ArgumentError`. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#stats-instance_method) for more. Returns a `RepositoryStats` covering size, file/blob counts, and (for a repository using compression) how much space it's saving.
505
471
 
506
472
  ## Key management
507
473
 
508
474
  ```ruby
509
- repo.key_add(user: "ci-runner")
475
+ repo.key_add(user: "ci-runner", new_password: [:file, "/etc/restic/ci-runner-password"])
510
476
  repo.key_list.each { |key| puts "#{key.id} #{key.user_name}@#{key.host_name} current=#{key.current?}" }
511
- repo.key_passwd(new_password_file: "/etc/restic/new-password")
477
+ repo.key_passwd(new_password: [:file, "/etc/restic/new-password"])
512
478
  repo.key_remove(id: old_key.id)
513
479
  ```
514
480
 
515
- `#key_add`/`#key_list`/`#key_passwd`/`#key_remove` (aliased `#add_key`/`#keys`/`#change_password`/`#remove_key`) manage passwords. Every key here is a different way to unlock the same underlying master key, not a separate encryption key of its own (see ["Low-level (`cat`) and object listing"](#low-level-cat-and-object-listing)'s note on `#cat_masterkey_and_game_over_if_this_leaks` below). `#key_passwd` doesn't mutate the `Repository` you called it on; build a new one with the rotated password to keep talking to the repository afterward.
481
+ `#key_add`/`#key_list`/`#key_passwd`/`#key_remove` (aliased `#add_key`/`#keys`/`#change_password`/`#remove_key`) manage passwords. Every key here is a different way to unlock the same underlying master key, not a separate encryption key of its own (see ["Low-level (`cat`) and object listing"](#low-level-cat-and-object-listing) below's note on `#cat_masterkey_and_game_over_if_this_leaks`).
516
482
 
517
- `#key_add`/`#key_passwd` share the same kwargs: `host:`/`user:` record a hostname/username on the new key (defaults to the OS's own, same as Restic itself); `new_insecure_no_password:` sets the new key's password to empty; `new_password_file:` reads the new password from a file instead of prompting interactively.
483
+ `#key_add`/`#key_passwd` share the same kwargs: `new_password:` is required, accepting the same shapes as `#initialize`'s `password:` minus `[:command, ...]`: a literal String; a `[:file, "..."]` tuple, resolved natively by Restic; `:insecure_no_password`; or anything responding to `#call`. Restic itself only accepts a new password by file, unlike the repository's own `password:`, so a literal String or callable is written to a briefly-lived, 0600-permissioned tempfile by Yobi first. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#key_add-instance_method) for the full list of options.
484
+
485
+ `#key_passwd` rotates the *password*, not the underlying master key itself (see the note on `#cat_masterkey_and_game_over_if_this_leaks` below if that distinction matters for your threat model). On success, it also updates this same `Repository` instance's own `password:` to match, so it keeps working against the repository right afterward:
486
+
487
+ ```ruby
488
+ repo.key_passwd(new_password: "new-password-value")
489
+ repo.snapshots # already using the new password - no need to rebuild `repo`
490
+ ```
518
491
 
519
492
  Restic refuses to `#key_remove` the key currently in use.
520
493
 
@@ -534,13 +507,21 @@ repo.list(:packs) # => Array of pack IDs
534
507
 
535
508
  ## Across repositories
536
509
 
510
+ ### `#init_mirror`
511
+
512
+ ```ruby
513
+ mirror_repo = primary_repo.init_mirror(url: "b2:my-bucket:/", password: "...")
514
+ ```
515
+
516
+ A shortcut for setting up a `#copy` destination: constructs a new `Repository` and initializes it with chunker parameters copied from `primary_repo`, so `#copy` between the two can deduplicate (skipped this way, Restic picks each repository's chunker polynomial independently at `#init` time, so nothing between them would ever dedupe). A one-time setup call, not an ongoing sync - pair it with repeated `#copy` calls below to actually move snapshots across.
517
+
537
518
  ### `#copy`
538
519
 
539
520
  ```ruby
540
- repo.copy(from_repo: source_repo, hosts: "web-1")
521
+ mirror_repo.copy(from_repo: primary_repo, tags: "nightly")
541
522
  ```
542
523
 
543
- Replicates snapshots from another repository into this one. `from_repo:` accepts a plain URL String or a `Repository` instance. Given the latter, its own `#url`/`#password` are used automatically unless `from_password:` is given explicitly. `from_key_hint:`/`from_repository_file:` are passed straight through to Restic (a key ID hint for the source repository, and reading the source URL from a file, respectively). Already-copied snapshots are skipped automatically on a repeat run. Returns `true`.
524
+ Replicates snapshots from another repository into this one. `from_repo:` accepts a plain URL String or a `Repository` instance. Given the latter, its own `#url`/`#password` are used automatically unless `from_password:` is given explicitly. Already-copied snapshots are skipped automatically on a repeat run. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#copy-instance_method) for the full list of options. Returns `true`.
544
525
 
545
526
  ## Mounting
546
527
 
@@ -552,7 +533,7 @@ repo.mount(mountpoint: "/mnt/restic") do |mount|
552
533
  end
553
534
  ```
554
535
 
555
- Serves the repository as a read-only FUSE filesystem at `mountpoint` (which must already exist). With a block, yields a `Yobi::Mount` and stops it automatically afterward. Without one, returns the `Mount` for you to `#stop` yourself:
536
+ Serves the repository as a read-only FUSE filesystem at `mountpoint` (which must already exist). With a block, yields a `Yobi::MountHandle` and stops it automatically afterward. Without one, returns the `MountHandle` for you to `#stop` yourself:
556
537
 
557
538
  ```ruby
558
539
  mount = repo.mount(mountpoint: "/mnt/restic")
@@ -563,17 +544,17 @@ ensure
563
544
  end
564
545
  ```
565
546
 
566
- `#stop` is safe to call more than once, and `Mount#pid`/`Mount#stop` are safe even if something *else* already triggered the unmount externally (`kill -INT <pid>`, or the OS's own `umount`/`fusermount` directly). `hosts:`/`paths:`/`tags:` restrict which snapshots appear under the mount's own `snapshots/` directory; `path_templates:`/`time_template:` control the directory naming scheme Restic generates there. `allow_other:`/`no_default_permissions:`/`owner_root:` are FUSE-level access options (allow other users to access the mount, skip permission checks, mount files as owned by root, respectively). `ready_timeout:` (default 10 seconds) bounds how long `#mount` waits for Restic's own readiness message before raising `Yobi::MountTimeout`.
547
+ `#stop` is safe to call more than once, and `MountHandle#pid`/`MountHandle#stop` are safe even if something *else* already triggered the unmount externally (`kill -INT <pid>`, or the OS's own `umount`/`fusermount` directly). `hosts:`/`paths:`/`tags:` restrict which snapshots appear under the mount's own `snapshots/` directory. `ready_timeout:` (default 10 seconds) bounds how long `#mount` waits for Restic's own readiness message before raising `Yobi::MountTimeout`. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Repository#mount-instance_method) for the full list of options.
567
548
 
568
549
  ## Restic-level (not scoped to a repository)
569
550
 
570
551
  ```ruby
571
552
  restic = Yobi::Restic.new
572
- restic.version # => {"version"=>"0.19.1", "go_version"=>"...", ...}
553
+ restic.version.version # => "0.19.1"
573
554
  restic.cache(cleanup: true)
574
555
  ```
575
556
 
576
- `#version` and `#cache` are the two Restic subcommands that don't touch a repository at all, so they live on `Yobi::Restic` rather than `Repository`. `#cache` lists (or, with `cleanup: true`, cleans) local cache directories, returning `true`. `max_age:` limits cleanup to caches older than the given duration; `no_size:` skips computing directory sizes, for a faster listing.
557
+ `#version` and `#cache` are the two Restic subcommands that don't touch a repository at all, so they live on `Yobi::Restic` rather than `Repository`. `#version` returns a `Yobi::ResticVersion` (`#version`, `#go_version`, `#go_os`, `#go_arch`); if the installed binary is old enough to ignore `--json` for this command entirely, Yobi falls back to parsing its plain-text output for the version number instead of raising a JSON parse error. `#cache` lists (or, with `cleanup: true`, cleans) local cache directories, returning `true`. See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Restic#cache-instance_method) for its remaining options.
577
558
 
578
559
  ## Global `Restic` settings
579
560
 
@@ -589,8 +570,10 @@ restic = Yobi::Restic.new(
589
570
 
590
571
  Two genuinely different kinds, both exposed as plain `attr_accessor`s:
591
572
 
592
- - **Env-var-backed**: `cache_dir:`, `compression:`, `pack_size:`, `read_concurrency:`, `host:`, `progress_fps:`, `cacert:`, `tls_client_cert:`, `key_hint:`. `Restic#env` computes the corresponding `RESTIC_*` vars fresh from the current accessor values on every call. Mutating one after construction is picked up by the very next command.
593
- - **CLI-flag-only, no env var equivalent**: `limit_download:`, `limit_upload:`, `retry_lock:`, `no_lock:`, `no_cache:`, `cleanup_cache:`, `no_extra_verify:`, `stuck_request_timeout:`, `options:` (Restic's own `--option`/`-o`, an escape hatch for backend-specific tuning like `s3.connections=10`), `http_user_agent:`, `quiet:`.
573
+ - **Env-var-backed** (e.g. `cache_dir:`, `compression:`, `pack_size:`): `Restic#env` computes the corresponding `RESTIC_*` vars fresh from the current accessor values on every call. Mutating one after construction is picked up by the very next command.
574
+ - **CLI-flag-only, no env var equivalent** (e.g. `limit_upload:`, `no_lock:`, `options:` - Restic's own `--option`/`-o`, an escape hatch for backend-specific tuning like `s3.connections=10`).
575
+
576
+ See the [YARD docs](https://rubydoc.info/gems/yobi/Yobi/Restic) for the full list of settings.
594
577
 
595
578
  "Picked up by the very next command" assumes calls happen one at a time. If you mutate one of these accessors on a `Restic` shared across threads while another thread has a command already in flight on it, which value that in-flight command actually uses is a race with no ordering guarantee, though not a crash: each accessor read/write is still atomic, so you won't get a corrupted argv, just an unpredictable choice between the old and new value.
596
579
 
@@ -642,9 +625,7 @@ outcome = repo.backup(
642
625
  exclude_caches: true
643
626
  )
644
627
 
645
- unless outcome.success?
646
- outcome.errors.each { |e| log_error("backup: #{e.item}: #{e.message}") }
647
- end
628
+ outcome.errors.each { |e| log_error("backup: #{e.item}: #{e.message}") }
648
629
 
649
630
  repo.forget(
650
631
  keep_daily: 7,
@@ -655,7 +636,7 @@ repo.forget(
655
636
  )
656
637
  ```
657
638
 
658
- `outcome.partial?` (exit code 3) means some files were skipped but the snapshot was still created (often fine to continue past, e.g. permission-denied on a handful of files); a full failure raises before `outcome` even exists via the usual `Yobi::ResticCommandFailed`/`Yobi::RepositoryLocked`/etc. hierarchy, so wrap the whole thing in the usual `rescue` if you want to alert on that separately (see ["Graceful lock-contention handling"](#graceful-lock-contention-handling) below).
639
+ A non-empty `outcome.errors` (exit code 3) means some files were skipped but the snapshot was still created (often fine to continue past, e.g. permission-denied on a handful of files); a full failure raises before `outcome` even exists via the usual `Yobi::ResticCommandFailed`/`Yobi::RepositoryLocked`/etc. hierarchy, so wrap the whole thing in the usual `rescue` if you want to alert on that separately (see ["Graceful lock-contention handling"](#graceful-lock-contention-handling) below).
659
640
 
660
641
  ### Restore workflows
661
642
 
@@ -683,8 +664,8 @@ end
683
664
  or list without mounting anything:
684
665
 
685
666
  ```ruby
686
- repo.ls(snapshot_id: "latest", dirs: "/var/myapp/uploads").entries.each do |entry|
687
- puts "#{entry.type} #{entry.size} #{entry.path}"
667
+ repo.ls(snapshot_id: "latest", dirs: "/var/myapp/uploads").nodes.each do |node|
668
+ puts "#{node.type} #{node.size} #{node.path}"
688
669
  end
689
670
  ```
690
671
 
@@ -812,11 +793,11 @@ end
812
793
  workers.each(&:join)
813
794
  ```
814
795
 
815
- **Copying between repositories** (e.g. replicating to a second, offsite backend for a 3-2-1 strategy):
796
+ **Copying between repositories** (e.g. replicating to a second, offsite backend for a 3-2-1 strategy), see `#init_mirror`/`#copy` under ["Across repositories"](#across-repositories):
816
797
 
817
798
  ```ruby
818
- offsite = Yobi::Repository.new(url: "b2:my-bucket:/", password: "...")
819
- offsite.copy(from_repo: primary_repo, tags: "nightly")
799
+ mirror_repo = primary_repo.init_mirror(url: "b2:my-bucket:/", password: "...") # once, before the first #copy
800
+ mirror_repo.copy(from_repo: primary_repo, tags: "nightly") # run nightly, e.g. from cron
820
801
  ```
821
802
 
822
803
  ### Repository maintenance and health checks
@@ -825,7 +806,7 @@ A separate, less-frequent job (weekly, say) than the nightly backup itself:
825
806
 
826
807
  ```ruby
827
808
  check = repo.check(read_data_subset: "10%")
828
- if check.report["num_errors"].to_i.positive?
809
+ if check.summary.num_errors.positive?
829
810
  check.errors.each { |e| AlertService.notify("Restic check: #{e.message}") }
830
811
  end
831
812
 
@@ -834,20 +815,6 @@ repo.prune(max_unused: "5%")
834
815
 
835
816
  `read_data_subset:` rotates through the repository over time (e.g. `"1/20"` today, `"2/20"` tomorrow) rather than reading everything in one run, so a full verification eventually happens without one job spending hours reading the entire dataset.
836
817
 
837
- ### Key rotation
838
-
839
- ```ruby
840
- repo.key_passwd(new_password_file: "/etc/restic/new-password")
841
-
842
- # repo itself still holds the OLD password - rebuild before continuing
843
- rotated_repo = Yobi::Repository.new(
844
- url: repo.url,
845
- password: [:file, "/etc/restic/new-password"]
846
- )
847
- ```
848
-
849
- This rotates the *password*, not the underlying encryption key itself (see ["Key management"](#key-management) above for why `repo` still needs rebuilding afterward). See ["Low-level (`cat`) and object listing"](#low-level-cat-and-object-listing)'s note on `#cat_masterkey_and_game_over_if_this_leaks` if that distinction matters for your threat model.
850
-
851
818
  ### Graceful lock-contention handling
852
819
 
853
820
  Two processes touching the same repository at once (a backup job and a prune job overlapping, say): retry instead of failing the whole run:
@@ -895,3 +862,5 @@ See `MAINTAINERS.md` for what to keep in sync as the API changes, and the releas
895
862
  ## License
896
863
 
897
864
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
865
+
866
+ ☂️☔️☂️☔️☂️
@@ -24,6 +24,11 @@ module Yobi
24
24
  @argv
25
25
  end
26
26
 
27
+ # @return [String]
28
+ def inspect
29
+ "#<#{self.class} #{@argv.inspect}>"
30
+ end
31
+
27
32
  # Appends one or more bare positional values. `nil` is dropped; an
28
33
  # Enumerable is flattened in; everything else is converted with `#to_s`.
29
34
  #
data/lib/yobi/errors.rb CHANGED
@@ -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
  # Base class for all errors raised by Yobi.
@@ -9,8 +8,8 @@ module Yobi
9
8
 
10
9
  # The `exit_error` message from a fatal Restic run, if one was printed.
11
10
  # https://restic.readthedocs.io/en/stable/075_scripting.html#exit-errors
12
- class ExitError < SimpleDelegator
13
- # Matches a JSON line with `"message_type": "exit_error"`.
11
+ class ExitError < Yobi::FancyHash
12
+ # @private
14
13
  LINE_PATTERN = /"message_type"\s*:\s*"exit_error"/
15
14
 
16
15
  # @param output [Yobi::ResticOutput]
@@ -37,8 +36,7 @@ module Yobi
37
36
  # @return [String]
38
37
  attr_reader :installed_version, :minimum_version
39
38
 
40
- # @param installed_version [String]
41
- # @param minimum_version [String]
39
+ # @private
42
40
  def initialize(installed_version:, minimum_version:)
43
41
  @installed_version = installed_version
44
42
  @minimum_version = minimum_version
@@ -53,8 +51,7 @@ module Yobi
53
51
  # @return [Array<String>]
54
52
  attr_reader :argv
55
53
 
56
- # @param restic_path [String]
57
- # @param argv [Array<String>]
54
+ # @private
58
55
  def initialize(restic_path:, argv:)
59
56
  @restic_path = restic_path
60
57
  @argv = argv
@@ -70,7 +67,7 @@ module Yobi
70
67
  # @return [Yobi::ExitError, nil]
71
68
  attr_reader :exit_error
72
69
 
73
- # @param execution [Hash]
70
+ # @private
74
71
  def initialize(execution)
75
72
  @execution = execution
76
73
  @exit_error = Yobi::ExitError.from_output(execution[:output])
@@ -98,8 +95,7 @@ module Yobi
98
95
  # @return [Numeric]
99
96
  attr_reader :timeout
100
97
 
101
- # @param argv [Array<String>]
102
- # @param timeout [Numeric]
98
+ # @private
103
99
  def initialize(argv:, timeout:)
104
100
  @argv = argv
105
101
  @timeout = timeout