yobi 0.3.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +11 -0
- data/README.md +42 -45
- data/lib/yobi/argv_builder.rb +1 -35
- data/lib/yobi/errors.rb +18 -30
- data/lib/yobi/fancy_hash.rb +1 -5
- data/lib/yobi/io_handle.rb +9 -17
- data/lib/yobi/mount_handle.rb +7 -11
- data/lib/yobi/repository/backup.rb +59 -90
- data/lib/yobi/repository/cat.rb +23 -43
- data/lib/yobi/repository/check.rb +21 -34
- data/lib/yobi/repository/copy.rb +11 -13
- data/lib/yobi/repository/diff.rb +18 -49
- data/lib/yobi/repository/dump.rb +14 -15
- data/lib/yobi/repository/find.rb +17 -37
- data/lib/yobi/repository/forget.rb +35 -39
- data/lib/yobi/repository/init.rb +19 -24
- data/lib/yobi/repository/key.rb +18 -33
- data/lib/yobi/repository/list.rb +5 -6
- data/lib/yobi/repository/ls.rb +20 -43
- data/lib/yobi/repository/migrate.rb +6 -6
- data/lib/yobi/repository/mount.rb +22 -32
- data/lib/yobi/repository/prune.rb +11 -9
- data/lib/yobi/repository/recover.rb +2 -4
- data/lib/yobi/repository/repair.rb +16 -23
- data/lib/yobi/repository/restore.rb +31 -54
- data/lib/yobi/repository/rewrite.rb +15 -20
- data/lib/yobi/repository/snapshots.rb +7 -8
- data/lib/yobi/repository/stats.rb +26 -21
- data/lib/yobi/repository/tag.rb +23 -33
- data/lib/yobi/repository/unlock.rb +2 -4
- data/lib/yobi/repository.rb +77 -24
- data/lib/yobi/restic.rb +73 -110
- data/lib/yobi/restic_output.rb +2 -74
- data/lib/yobi/snapshot.rb +15 -27
- data/lib/yobi/version.rb +1 -2
- data/sig/yobi.rbs +4 -4
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4daa53c0b99656d99f3b81b808ad6940ad0fa6b8d202f0915b769721972a40a2
|
|
4
|
+
data.tar.gz: c1931310b0b9081099dedd3251cc304f01f8938ca0de13be82801289af1080db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbb9ae518fd0181e5c7cff763f74f113936cefa996eb2ab5bd741e5c61550b5a254cb6b795aadc67363cfdc064351aa2b77f830cb8f6dadde4372974dc1dffc5
|
|
7
|
+
data.tar.gz: 888067f9352c2a6bfe4493e7ea28770abc9df716c88ead2228b319ef7f07604884e977c1205e8726df61673ce0c79cf7f71d613296a6145d51af92ef72353ccf
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## [1.0.0] - 2026-08-29
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Breaking:** minimum supported Restic bumped from 0.17.1 to 0.18.0. Restic 0.17.x completely ignored `--json` for `tag` and `check`, so those commands ran without error but returned empty `TagOutcome`/`CheckOutcome` objects. 0.18.0 is the first version emitting structured JSON for both. `Yobi::UnsupportedResticVersion` is now raised against any 0.17.x binary.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Restic#restic_path=` setter.
|
|
10
|
+
- `Repository#url=`, `#password=`, and `#backend_credentials=` setters, validated on assignment with the same shapes `#initialize` accepts.
|
|
11
|
+
|
|
1
12
|
## [0.3.1] - 2026-08-10
|
|
2
13
|
|
|
3
14
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Yobi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/yobi)
|
|
4
|
+
|
|
5
|
+
A Ruby interface for the [Restic](https://restic.net/) backup program.
|
|
6
|
+
|
|
7
|
+
Tested against Restic 0.18.0, 0.18.1, 0.19.0, 0.19.1.
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
@@ -30,7 +34,7 @@ restic version
|
|
|
30
34
|
|
|
31
35
|
### Minimum supported version
|
|
32
36
|
|
|
33
|
-
Yobi requires Restic **0.
|
|
37
|
+
Yobi requires Restic **0.18.0** or newer. Its typed errors (`Yobi::RepositoryNotFound`, `RepositoryLocked`, `AuthenticationFailed`) rely on stable exit codes, and its `#tag`/`#check` outcomes rely on structured `--json` output that older restic didn't emit for those commands. Yobi checks the installed version before running any real command and raises `Yobi::UnsupportedResticVersion` if it's too old:
|
|
34
38
|
|
|
35
39
|
```ruby
|
|
36
40
|
begin
|
|
@@ -40,7 +44,7 @@ rescue Yobi::UnsupportedResticVersion => e
|
|
|
40
44
|
end
|
|
41
45
|
```
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
If you'd rather fail fast at application startup instead of on the first backup attempt, call it explicitly:
|
|
44
48
|
|
|
45
49
|
```ruby
|
|
46
50
|
RESTIC = Yobi::Restic.new
|
|
@@ -230,7 +234,7 @@ repo.init
|
|
|
230
234
|
# => #<Yobi::Initialized id="..." repository="...">
|
|
231
235
|
```
|
|
232
236
|
|
|
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 [
|
|
237
|
+
`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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#init-instance_method) for the full list of options. Returns an `Initialized` (`#id`/`#repository`).
|
|
234
238
|
|
|
235
239
|
### `#cat_config`
|
|
236
240
|
|
|
@@ -261,7 +265,7 @@ outcome.summary["data_added"]
|
|
|
261
265
|
|
|
262
266
|
`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).
|
|
263
267
|
|
|
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 [
|
|
268
|
+
`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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#backup-instance_method) for the full list of options (filtering, retention, timing, and more).
|
|
265
269
|
|
|
266
270
|
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).
|
|
267
271
|
|
|
@@ -291,7 +295,7 @@ outcome.summary["snapshot_id"]
|
|
|
291
295
|
repo.restore(snapshot_id: "latest", target: "/tmp/restore")
|
|
292
296
|
```
|
|
293
297
|
|
|
294
|
-
`snapshot_id:` accepts `"latest"` or a real ID; `target:` is the destination directory. `delete:` removes files in `target:` not present in the snapshot -
|
|
298
|
+
`snapshot_id:` accepts `"latest"` or a real ID; `target:` is the destination directory. `delete:` removes files in `target:` not present in the snapshot - the one option here that can destroy data outside the snapshot itself. See the [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#restore-instance_method) for the full list of options (filtering, `overwrite:` behavior, and more).
|
|
295
299
|
|
|
296
300
|
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.
|
|
297
301
|
|
|
@@ -315,7 +319,7 @@ repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.tar")
|
|
|
315
319
|
repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.zip", archive: "zip")
|
|
316
320
|
```
|
|
317
321
|
|
|
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 [
|
|
322
|
+
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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#dump-instance_method) for the full list of options.
|
|
319
323
|
|
|
320
324
|
```ruby
|
|
321
325
|
handle = repo.dump(snapshot_id: "latest", file: "/var/www")
|
|
@@ -335,7 +339,7 @@ outcome.statistics.changed_files
|
|
|
335
339
|
outcome.changes.each { |change| puts "#{change.modifier} #{change.path}" }
|
|
336
340
|
```
|
|
337
341
|
|
|
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 [
|
|
342
|
+
`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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#diff-instance_method) for the full list of options.
|
|
339
343
|
|
|
340
344
|
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`).
|
|
341
345
|
|
|
@@ -349,7 +353,7 @@ repo.snapshots(tags: ["daily"], hosts: "web-1").each do |snapshot|
|
|
|
349
353
|
end
|
|
350
354
|
```
|
|
351
355
|
|
|
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 [
|
|
356
|
+
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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#snapshots-instance_method) for the full list of options.
|
|
353
357
|
|
|
354
358
|
### `#tag`
|
|
355
359
|
|
|
@@ -357,7 +361,7 @@ Returns an Enumerable of `Snapshot` (`#id`, `#short_id`, `#time`, `#host`, `#tag
|
|
|
357
361
|
repo.tag(snapshot_ids: snapshot.id, add: "verified")
|
|
358
362
|
```
|
|
359
363
|
|
|
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 [
|
|
364
|
+
`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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#tag-instance_method) for the full list of options.
|
|
361
365
|
|
|
362
366
|
Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#changed_snapshots`) and lazy `#changes` (Enumerable of `TagChange`, the old-ID/new-ID pairs).
|
|
363
367
|
|
|
@@ -367,7 +371,7 @@ Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#c
|
|
|
367
371
|
repo.forget(keep_daily: 7, keep_weekly: 4, keep_monthly: 12, prune: true)
|
|
368
372
|
```
|
|
369
373
|
|
|
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 [
|
|
374
|
+
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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#forget-instance_method) for the full list of options.
|
|
371
375
|
|
|
372
376
|
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"`).
|
|
373
377
|
|
|
@@ -380,9 +384,9 @@ repo.find(patterns: "*.pem").each do |matches|
|
|
|
380
384
|
end
|
|
381
385
|
```
|
|
382
386
|
|
|
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 [
|
|
387
|
+
Searches for files/directories by name pattern across snapshots. `blob:`/`pack:`/`tree:` switch to matching object IDs instead, for low-level troubleshooting. See the [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#find-instance_method) for the full list of options.
|
|
384
388
|
|
|
385
|
-
Returns an Enumerable of `MatchesPerSnapshot
|
|
389
|
+
Returns an Enumerable of `MatchesPerSnapshot`, one per snapshot with matches. 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.).
|
|
386
390
|
|
|
387
391
|
### `#ls`
|
|
388
392
|
|
|
@@ -392,7 +396,7 @@ outcome.snapshot.short_id
|
|
|
392
396
|
outcome.nodes.each { |node| puts node.path }
|
|
393
397
|
```
|
|
394
398
|
|
|
395
|
-
Lists a snapshot's files/directories. `recursive:` descends into subdirectories. See the [
|
|
399
|
+
Lists a snapshot's files/directories. `recursive:` descends into subdirectories. See the [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#ls-instance_method) for the full list of options.
|
|
396
400
|
|
|
397
401
|
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).
|
|
398
402
|
|
|
@@ -406,7 +410,7 @@ outcome.summary.num_errors
|
|
|
406
410
|
outcome.errors.each { |error| puts error.message }
|
|
407
411
|
```
|
|
408
412
|
|
|
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 [
|
|
413
|
+
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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#check-instance_method) for the full list of options.
|
|
410
414
|
|
|
411
415
|
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.
|
|
412
416
|
|
|
@@ -416,7 +420,7 @@ Returns a `CheckOutcome`: `#summary` (aliased `#report`; a `CheckSummary` coveri
|
|
|
416
420
|
repo.prune(max_unused: "5%")
|
|
417
421
|
```
|
|
418
422
|
|
|
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 [
|
|
423
|
+
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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#prune-instance_method) for the full list of options. Returns `true`.
|
|
420
424
|
|
|
421
425
|
### `#repair_index`, `#repair_packs`, `#repair_snapshots`
|
|
422
426
|
|
|
@@ -426,7 +430,7 @@ repo.repair_packs(ids: damaged_pack_ids)
|
|
|
426
430
|
repo.repair_snapshots(snapshot_ids: affected_ids, forget: true)
|
|
427
431
|
```
|
|
428
432
|
|
|
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
|
|
433
|
+
`#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 - depends on a correct index, so run `#repair_index` first. See the [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#repair_snapshots-instance_method) for the full list of options.
|
|
430
434
|
|
|
431
435
|
### `#recover`
|
|
432
436
|
|
|
@@ -434,7 +438,7 @@ repo.repair_snapshots(snapshot_ids: affected_ids, forget: true)
|
|
|
434
438
|
repo.recover
|
|
435
439
|
```
|
|
436
440
|
|
|
437
|
-
Builds a new snapshot from any data present in the repository but not referenced by an existing snapshot (e.g. after an accidental `#forget`). Call `#snapshots` afterward to see whether anything was actually recovered.
|
|
441
|
+
Builds a new snapshot from any data present in the repository but not referenced by an existing snapshot (e.g. after an accidental `#forget`). Call `#snapshots` afterward to see whether anything was actually recovered.
|
|
438
442
|
|
|
439
443
|
### `#rewrite`
|
|
440
444
|
|
|
@@ -442,7 +446,7 @@ Builds a new snapshot from any data present in the repository but not referenced
|
|
|
442
446
|
repo.rewrite(snapshot_ids: old.id, excludes: "*.log", new_time: Time.now.iso8601)
|
|
443
447
|
```
|
|
444
448
|
|
|
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
|
|
449
|
+
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. `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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#rewrite-instance_method) for the full list of options.
|
|
446
450
|
|
|
447
451
|
### `#migrate`
|
|
448
452
|
|
|
@@ -467,7 +471,7 @@ repo.stats(mode: "raw-data")
|
|
|
467
471
|
# => #<Yobi::RepositoryStats total_size=... total_file_count=... snapshots_count=...>
|
|
468
472
|
```
|
|
469
473
|
|
|
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 [
|
|
474
|
+
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 [docs](https://rubydoc.info/gems/yobi/1.0.0/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.
|
|
471
475
|
|
|
472
476
|
## Key management
|
|
473
477
|
|
|
@@ -480,9 +484,9 @@ repo.key_remove(id: old_key.id)
|
|
|
480
484
|
|
|
481
485
|
`#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`).
|
|
482
486
|
|
|
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 [
|
|
487
|
+
`#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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#key_add-instance_method) for the full list of options.
|
|
484
488
|
|
|
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
|
|
489
|
+
`#key_passwd` rotates the *password*, not the underlying master key itself (see the note on `#cat_masterkey_and_game_over_if_this_leaks` below). On success, it also updates this same `Repository` instance's own `password:` to match, so it keeps working against the repository right afterward:
|
|
486
490
|
|
|
487
491
|
```ruby
|
|
488
492
|
repo.key_passwd(new_password: "new-password-value")
|
|
@@ -503,7 +507,7 @@ repo.list(:packs) # => Array of pack IDs
|
|
|
503
507
|
|
|
504
508
|
`#cat_snapshot`/`#cat_index`/`#cat_key`/`#cat_tree` return a repository object's raw stored record as a Hash, lower-level than `#snapshots`/`#key_list`, which wrap entries in `Snapshot`/`Key` instead. `#cat_snapshot`/`#cat_key`/`#cat_tree` each accept either a bare ID String or the corresponding wrapper object (`Snapshot`/`Key`/`Snapshot`) directly; `#cat_index` only takes a bare ID String, since index entries have no wrapper class of their own. `#cat_pack`/`#cat_blob` are raw-bytes commands with the same block/`IOHandle` shape as `#dump`. `#list(type)` enumerates every object ID of a given type (`:blobs`/`:packs`/`:index`/`:snapshots`/`:keys`/`:locks`) as plain strings. Restic ignores `--json` for this command, so these come back exactly as Restic prints them.
|
|
505
509
|
|
|
506
|
-
`#cat_masterkey_and_game_over_if_this_leaks` returns the repository's actual encryption/MAC key material: not a redacted reference, the real thing. There is no operation that rotates this key;
|
|
510
|
+
`#cat_masterkey_and_game_over_if_this_leaks` returns the repository's actual encryption/MAC key material: not a redacted reference, the real thing. There is no operation that rotates this key; a leak means every past snapshot in the repository stays decryptable forever, no password change fixes that.
|
|
507
511
|
|
|
508
512
|
## Across repositories
|
|
509
513
|
|
|
@@ -521,7 +525,7 @@ A shortcut for setting up a `#copy` destination: constructs a new `Repository` a
|
|
|
521
525
|
mirror_repo.copy(from_repo: primary_repo, tags: "nightly")
|
|
522
526
|
```
|
|
523
527
|
|
|
524
|
-
Replicates snapshots from another repository into this one. `from_repo:` accepts a plain URL String, a `[:file, "..."]` tuple reading the URL from a file, 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 [
|
|
528
|
+
Replicates snapshots from another repository into this one. `from_repo:` accepts a plain URL String, a `[:file, "..."]` tuple reading the URL from a file, 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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#copy-instance_method) for the full list of options. Returns `true`.
|
|
525
529
|
|
|
526
530
|
## Mounting
|
|
527
531
|
|
|
@@ -544,7 +548,7 @@ ensure
|
|
|
544
548
|
end
|
|
545
549
|
```
|
|
546
550
|
|
|
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 [
|
|
551
|
+
`#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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Repository#mount-instance_method) for the full list of options.
|
|
548
552
|
|
|
549
553
|
## Restic-level (not scoped to a repository)
|
|
550
554
|
|
|
@@ -554,11 +558,11 @@ restic.version.version # => "0.19.1"
|
|
|
554
558
|
restic.cache(cleanup: true)
|
|
555
559
|
```
|
|
556
560
|
|
|
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 [
|
|
561
|
+
`#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 [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Restic#cache-instance_method) for its remaining options.
|
|
558
562
|
|
|
559
563
|
## Global `Restic` settings
|
|
560
564
|
|
|
561
|
-
|
|
565
|
+
`Yobi::Restic` holds settings that apply to every command, regardless of repository:
|
|
562
566
|
|
|
563
567
|
```ruby
|
|
564
568
|
restic = Yobi::Restic.new(
|
|
@@ -568,14 +572,9 @@ restic = Yobi::Restic.new(
|
|
|
568
572
|
)
|
|
569
573
|
```
|
|
570
574
|
|
|
571
|
-
|
|
572
|
-
|
|
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
|
+
Some map to `RESTIC_*` env vars (`cache_dir:`, `compression:`, `pack_size:`). Others are CLI flags with no env equivalent (`limit_upload:`, `no_lock:`, `options:`, etc.). `options:` maps to Restic's `-o` for backend tuning like `s3.connections=10`.
|
|
575
576
|
|
|
576
|
-
|
|
577
|
-
|
|
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.
|
|
577
|
+
Every setting is an `attr_accessor`. Change one and the next command sees the new value. Full list in the [docs](https://rubydoc.info/gems/yobi/1.0.0/Yobi/Restic).
|
|
579
578
|
|
|
580
579
|
### Sharing a `Restic` across repositories
|
|
581
580
|
|
|
@@ -586,11 +585,9 @@ customer_a = Yobi::Repository.new(url: "s3:.../customer-a", password: "...", res
|
|
|
586
585
|
customer_b = Yobi::Repository.new(url: "s3:.../customer-b", password: "...", restic: restic)
|
|
587
586
|
```
|
|
588
587
|
|
|
589
|
-
Both
|
|
590
|
-
|
|
591
|
-
### Minimum-version enforcement, in detail
|
|
588
|
+
Both use the same settings. Changing `restic.limit_upload` later affects every repository built from it.
|
|
592
589
|
|
|
593
|
-
`
|
|
590
|
+
`--limit-upload` is per-invocation. Two backups running concurrently through the same `restic` are each capped at 5000. They don't share one 5000 budget.
|
|
594
591
|
|
|
595
592
|
## Redaction
|
|
596
593
|
|
|
@@ -601,7 +598,7 @@ repo.inspect
|
|
|
601
598
|
# => #<Yobi::Repository url="s3:..." password="[FILTERED]" backend_credentials={"AWS_ACCESS_KEY_ID"=>"[FILTERED]", "AWS_SECRET_ACCESS_KEY"=>"[FILTERED]"}>
|
|
602
599
|
```
|
|
603
600
|
|
|
604
|
-
A literal or command/file-sourced `password:` shows as `"[FILTERED]"`; anything callable shows as `"[RESOLVER]"` **without ever being called**.
|
|
601
|
+
A literal or command/file-sourced `password:` shows as `"[FILTERED]"`; anything callable shows as `"[RESOLVER]"` **without ever being called**. `:insecure_no_password` shows plainly, since it's an explicit "there is no secret" declaration, not a value that could leak anything. `backend_credentials:` keys not in Restic's own documented list of non-identity operational env vars (`Yobi::Restic::ALLOWED_ENV_VARS`) are filtered the same way; the allowed ones (`RESTIC_CACHE_DIR`, `AWS_DEFAULT_REGION`, and similar non-secret operational vars) stay visible.
|
|
605
602
|
|
|
606
603
|
`#env` itself is never redacted. It's the method that actually builds what gets passed to `Process.spawn`, so it has to return the real values. Only `#inspect` (the thing a stray debug print might accidentally call) filters.
|
|
607
604
|
|
|
@@ -636,7 +633,7 @@ repo.forget(
|
|
|
636
633
|
)
|
|
637
634
|
```
|
|
638
635
|
|
|
639
|
-
A non-empty `outcome.errors` (exit code 3) means some files were skipped but the snapshot was still created
|
|
636
|
+
A non-empty `outcome.errors` (exit code 3) means some files were skipped but the snapshot was still created - often fine to continue past. A full failure raises `Yobi::ResticCommandFailed`/`Yobi::RepositoryLocked`/etc. before `outcome` exists at all; handle those with a `rescue` around the block (see ["Graceful lock-contention handling"](#graceful-lock-contention-handling)).
|
|
640
637
|
|
|
641
638
|
### Restore workflows
|
|
642
639
|
|
|
@@ -704,7 +701,7 @@ password: -> {
|
|
|
704
701
|
}
|
|
705
702
|
```
|
|
706
703
|
|
|
707
|
-
If the resolver itself is slow or rate-limited (a network round-trip per Restic invocation adds up over many commands), add your own caching inside the lambda
|
|
704
|
+
If the resolver itself is slow or rate-limited (a network round-trip per Restic invocation adds up over many commands), add your own caching inside the lambda.
|
|
708
705
|
|
|
709
706
|
### Database dumps via `stdin_from_command`
|
|
710
707
|
|
|
@@ -717,7 +714,7 @@ repo.backup(
|
|
|
717
714
|
)
|
|
718
715
|
```
|
|
719
716
|
|
|
720
|
-
The third element (`"mydb.sql"`) names the resulting virtual file inside the snapshot. Omit it
|
|
717
|
+
The third element (`"mydb.sql"`) names the resulting virtual file inside the snapshot. Omit it for Restic's own `"stdin"` default. Use the Array form (`["mysqldump", "-u", user, db]`) when any argument contains spaces, or when any part comes from untrusted input - the String form is tokenized with `Shellwords.split`, which will split spaces inside an argument and, on untrusted input, let it inject extra arguments.
|
|
721
718
|
|
|
722
719
|
```ruby
|
|
723
720
|
repo.backup(source: [:stdin_from_command, ["mysqldump", "-u", "backup", "my app db"]])
|
|
@@ -772,7 +769,7 @@ Customer.find_each do |customer|
|
|
|
772
769
|
end
|
|
773
770
|
```
|
|
774
771
|
|
|
775
|
-
|
|
772
|
+
To run backups concurrently, use a bounded worker pool. It caps how many `restic` subprocesses run at once and catches per-customer errors that would otherwise disappear with the thread:
|
|
776
773
|
|
|
777
774
|
```ruby
|
|
778
775
|
queue = Queue.new
|
|
@@ -836,7 +833,7 @@ end
|
|
|
836
833
|
with_retry { repo.backup(source: source_path) }
|
|
837
834
|
```
|
|
838
835
|
|
|
839
|
-
For a one-off manual fix
|
|
836
|
+
For a one-off manual fix, `repo.unlock` removes stale locks left by a process that crashed without cleaning up. Don't put it inside the retry loop above - a lock might belong to a live second process, not a dead one.
|
|
840
837
|
|
|
841
838
|
## Non-goals
|
|
842
839
|
|
|
@@ -855,7 +852,7 @@ After checking out the repo, run `bin/setup` to install dependencies.
|
|
|
855
852
|
- `bin/test`: both, in parallel.
|
|
856
853
|
- `bin/standardrb`: lint.
|
|
857
854
|
- `bin/console`: an interactive prompt with the gem loaded.
|
|
858
|
-
- `bin/docs`:
|
|
855
|
+
- `bin/docs`: regenerates RDoc into `./doc` and opens the index in the default browser.
|
|
859
856
|
|
|
860
857
|
See `MAINTAINERS.md` for what to keep in sync as the API changes, and the release checklist.
|
|
861
858
|
|
data/lib/yobi/argv_builder.rb
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
|
-
|
|
5
|
-
# only the methods below are available; {#to_a} hands back the plain
|
|
6
|
-
# Array once building is done.
|
|
7
|
-
#
|
|
8
|
-
# @private
|
|
9
|
-
class ArgvBuilder
|
|
10
|
-
# Maps a flag name Symbol to its "--dashed-string" form, e.g.
|
|
11
|
-
# `:read_data_subset` to `"--read-data-subset"`.
|
|
4
|
+
class ArgvBuilder # :nodoc:
|
|
12
5
|
FLAGS = Hash.new { |flags, name| flags[name] = "--#{name.to_s.tr("_", "-")}" }
|
|
13
|
-
|
|
14
|
-
# Maps a short flag name Symbol to its "-name" form, e.g. `:vv` to `"-vv"`.
|
|
15
6
|
SHORT_FLAGS = Hash.new { |flags, name| flags[name] = "-#{name}" }
|
|
16
7
|
|
|
17
8
|
def initialize
|
|
@@ -19,21 +10,14 @@ module Yobi
|
|
|
19
10
|
@end_of_options_called = false
|
|
20
11
|
end
|
|
21
12
|
|
|
22
|
-
# @return [Array<String>] the argv built so far
|
|
23
13
|
def to_a
|
|
24
14
|
@argv
|
|
25
15
|
end
|
|
26
16
|
|
|
27
|
-
# @return [String]
|
|
28
17
|
def inspect
|
|
29
18
|
"#<#{self.class} #{@argv.inspect}>"
|
|
30
19
|
end
|
|
31
20
|
|
|
32
|
-
# Appends one or more bare positional values. `nil` is dropped; an
|
|
33
|
-
# Enumerable is flattened in; everything else is converted with `#to_s`.
|
|
34
|
-
#
|
|
35
|
-
# @param values [Array<Object>]
|
|
36
|
-
# @return [self]
|
|
37
21
|
def append(*values)
|
|
38
22
|
values.each do |v|
|
|
39
23
|
case v
|
|
@@ -49,41 +33,23 @@ module Yobi
|
|
|
49
33
|
self
|
|
50
34
|
end
|
|
51
35
|
|
|
52
|
-
# Appends a flag, plus its value when one is given.
|
|
53
|
-
#
|
|
54
|
-
# @param name [Symbol] flag name, looked up in {FLAGS}
|
|
55
|
-
# @param value [Object, nil] the flag's value; omit for a boolean flag
|
|
56
|
-
# @return [self]
|
|
57
36
|
def flag(name, value = nil)
|
|
58
37
|
@argv << FLAGS[name]
|
|
59
38
|
@argv << value.to_s unless value.nil?
|
|
60
39
|
self
|
|
61
40
|
end
|
|
62
41
|
|
|
63
|
-
# Appends a repeatable flag once per value, e.g. `--host a --host b`.
|
|
64
|
-
#
|
|
65
|
-
# @param name [Symbol] flag name, looked up in {FLAGS}
|
|
66
|
-
# @param values [Array<Object>, Object, nil]
|
|
67
|
-
# @return [self]
|
|
68
42
|
def repeat_flag(name, values)
|
|
69
43
|
name = FLAGS[name]
|
|
70
44
|
array_of_strings(values).each { |value| @argv << name << value }
|
|
71
45
|
self
|
|
72
46
|
end
|
|
73
47
|
|
|
74
|
-
# Appends a short flag, e.g. `short_flag(:vv)` for `-vv`.
|
|
75
|
-
#
|
|
76
|
-
# @param name [Symbol] flag name, looked up in {SHORT_FLAGS}
|
|
77
|
-
# @return [self]
|
|
78
48
|
def short_flag(name)
|
|
79
49
|
@argv << SHORT_FLAGS[name]
|
|
80
50
|
self
|
|
81
51
|
end
|
|
82
52
|
|
|
83
|
-
# Appends Restic's `--` end-of-options marker.
|
|
84
|
-
#
|
|
85
|
-
# @return [self]
|
|
86
|
-
# @raise [RuntimeError] if called more than once on the same builder
|
|
87
53
|
def end_of_options
|
|
88
54
|
raise "end_of_options already called - Restic only honors the first \"--\"" if @end_of_options_called
|
|
89
55
|
|
data/lib/yobi/errors.rb
CHANGED
|
@@ -6,38 +6,35 @@ module Yobi
|
|
|
6
6
|
# Base class for all errors raised by Yobi.
|
|
7
7
|
class Error < StandardError; end
|
|
8
8
|
|
|
9
|
-
# The
|
|
10
|
-
# https://restic.readthedocs.io/en/stable/075_scripting.html#exit-errors
|
|
9
|
+
# The +exit_error+ message from a fatal Restic run, if one was printed.
|
|
10
|
+
# See https://restic.readthedocs.io/en/stable/075_scripting.html#exit-errors.
|
|
11
11
|
class ExitError < Yobi::FancyHash
|
|
12
|
-
#
|
|
13
|
-
LINE_PATTERN = /"message_type"\s*:\s*"exit_error"/
|
|
12
|
+
LINE_PATTERN = /"message_type"\s*:\s*"exit_error"/ # :nodoc:
|
|
14
13
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Builds an ExitError from a Yobi::ResticOutput, or +nil+ if no
|
|
15
|
+
# +exit_error+ line is present in it.
|
|
17
16
|
def self.from_output(output)
|
|
18
17
|
line = output.each_line.find { |candidate| LINE_PATTERN.match?(candidate) }
|
|
19
18
|
new(JSON.parse(line)) if line
|
|
20
19
|
end
|
|
21
20
|
|
|
22
|
-
#
|
|
21
|
+
# Restic's own +exit_error+ code String, if present.
|
|
23
22
|
def code
|
|
24
23
|
self["code"]
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
#
|
|
26
|
+
# Restic's own +exit_error+ message String, if present.
|
|
28
27
|
def message
|
|
29
28
|
self["message"]
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
# Raised when the installed Restic binary is older than
|
|
34
|
-
#
|
|
33
|
+
# Yobi::Restic::MINIMUM_VERSION.
|
|
35
34
|
class UnsupportedResticVersion < Error
|
|
36
|
-
# @return [String]
|
|
37
35
|
attr_reader :installed_version, :minimum_version
|
|
38
36
|
|
|
39
|
-
#
|
|
40
|
-
def initialize(installed_version:, minimum_version:)
|
|
37
|
+
def initialize(installed_version:, minimum_version:) # :nodoc:
|
|
41
38
|
@installed_version = installed_version
|
|
42
39
|
@minimum_version = minimum_version
|
|
43
40
|
super("Restic #{installed_version} does not meet the minimum supported version #{minimum_version}")
|
|
@@ -46,13 +43,9 @@ module Yobi
|
|
|
46
43
|
|
|
47
44
|
# Raised when the Restic binary itself can't be found or executed.
|
|
48
45
|
class ResticNotFound < Error
|
|
49
|
-
|
|
50
|
-
attr_reader :restic_path
|
|
51
|
-
# @return [Array<String>]
|
|
52
|
-
attr_reader :argv
|
|
46
|
+
attr_reader :restic_path, :argv
|
|
53
47
|
|
|
54
|
-
#
|
|
55
|
-
def initialize(restic_path:, argv:)
|
|
48
|
+
def initialize(restic_path:, argv:) # :nodoc:
|
|
56
49
|
@restic_path = restic_path
|
|
57
50
|
@argv = argv
|
|
58
51
|
super("Restic binary not found: #{restic_path.inspect} (tried to run #{argv.inspect})")
|
|
@@ -62,13 +55,12 @@ module Yobi
|
|
|
62
55
|
# Raised when Restic exits with a failure code. Base class for the
|
|
63
56
|
# specific typed errors below.
|
|
64
57
|
class ResticExecutionError < Error
|
|
65
|
-
#
|
|
58
|
+
# The raw +{exit_code:, output:, argv:}+ execution result.
|
|
66
59
|
attr_reader :execution
|
|
67
|
-
#
|
|
60
|
+
# A Yobi::ExitError parsed out of the output, if one is present.
|
|
68
61
|
attr_reader :exit_error
|
|
69
62
|
|
|
70
|
-
#
|
|
71
|
-
def initialize(execution)
|
|
63
|
+
def initialize(execution) # :nodoc:
|
|
72
64
|
@execution = execution
|
|
73
65
|
@exit_error = Yobi::ExitError.from_output(execution[:output])
|
|
74
66
|
super(exit_error&.message || "Restic exited with status #{execution[:exit_code]}: #{execution[:output]}")
|
|
@@ -87,16 +79,12 @@ module Yobi
|
|
|
87
79
|
# Raised when Restic exits with a failure code not otherwise classified above.
|
|
88
80
|
class ResticCommandFailed < ResticExecutionError; end
|
|
89
81
|
|
|
90
|
-
# Raised by
|
|
91
|
-
# ready within
|
|
82
|
+
# Raised by Yobi::Repository#mount when Restic doesn't report itself
|
|
83
|
+
# ready within +ready_timeout:+ seconds.
|
|
92
84
|
class MountTimeout < Error
|
|
93
|
-
|
|
94
|
-
attr_reader :argv
|
|
95
|
-
# @return [Numeric]
|
|
96
|
-
attr_reader :timeout
|
|
85
|
+
attr_reader :argv, :timeout
|
|
97
86
|
|
|
98
|
-
#
|
|
99
|
-
def initialize(argv:, timeout:)
|
|
87
|
+
def initialize(argv:, timeout:) # :nodoc:
|
|
100
88
|
@argv = argv
|
|
101
89
|
@timeout = timeout
|
|
102
90
|
super("Restic mount didn't report readiness within #{timeout}s (tried to run #{argv.inspect})")
|
data/lib/yobi/fancy_hash.rb
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Yobi
|
|
4
|
-
|
|
5
|
-
class FancyHash < Hash
|
|
6
|
-
# @private
|
|
4
|
+
class FancyHash < Hash # :nodoc:
|
|
7
5
|
def initialize(raw)
|
|
8
6
|
super()
|
|
9
7
|
replace(raw)
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
# @return [String]
|
|
13
10
|
def inspect
|
|
14
11
|
"#<#{self.class} #{super}>"
|
|
15
12
|
end
|
|
16
13
|
|
|
17
|
-
# @return [void]
|
|
18
14
|
def pretty_print(q)
|
|
19
15
|
q.object_group(self) do
|
|
20
16
|
each do |key, value|
|