yobi 0.3.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +17 -0
- data/README.md +83 -44
- data/lib/yobi/argv_builder.rb +1 -35
- data/lib/yobi/cancellable_proxy.rb +48 -0
- data/lib/yobi/cancellation.rb +97 -0
- data/lib/yobi/errors.rb +45 -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 +96 -25
- data/lib/yobi/restic.rb +93 -114
- data/lib/yobi/restic_output.rb +2 -74
- data/lib/yobi/snapshot.rb +15 -27
- data/lib/yobi/version.rb +1 -2
- data/lib/yobi.rb +2 -0
- data/sig/yobi.rbs +83 -6
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cea1c8c245d4ba7aec5a218441f19921606c7bdb5d2b1ddc9912c2d9519ba15e
|
|
4
|
+
data.tar.gz: 66bf1c1c5e4ce94706948db27352889b8af41e6967c0c00406c303515181f710
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89d14f78cd56cfcd7e48e3d56b94a2f5159ac5de5877872797b106307730d0d8c98702fd897ac5572f72d294fede4726e59cecfeab942e1fc29c1e15fe4184f3
|
|
7
|
+
data.tar.gz: d2c8ae8d702cebc1186c0f117abb542d1e80e84d4dc5e0ccac7ced97b278cd0762c0b70dfe2db84d968529aa235f12fbfd34b07252aa140d6bc2841cab372221
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [1.1.0] - 2026-09-02
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `Repository#with_cancellation(token)` and `Yobi::Cancellation`: stop a long-running `#backup`/`#restore`/`#check`/`#prune`/`#forget`/`#copy` from another thread via `token.cancel!`, raising `Yobi::Cancelled` if Restic exited with code 130.
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `Restic#restic_path=` setter.
|
|
16
|
+
- `Repository#url=`, `#password=`, and `#backend_credentials=` setters, validated on assignment with the same shapes `#initialize` accepts.
|
|
17
|
+
|
|
1
18
|
## [0.3.1] - 2026-08-10
|
|
2
19
|
|
|
3
20
|
### 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
|
|
@@ -216,6 +220,7 @@ The full hierarchy, all under `Yobi::Error < StandardError`:
|
|
|
216
220
|
- `Yobi::RepositoryNotFound`, `Yobi::RepositoryLocked`, `Yobi::AuthenticationFailed`: Restic's own typed exit codes (10/11/12).
|
|
217
221
|
- `Yobi::ResticCommandFailed`: any other non-zero exit, for a failure that doesn't fit one of the above.
|
|
218
222
|
- `Yobi::MountTimeout`: `#mount` didn't report itself ready within its timeout.
|
|
223
|
+
- `Yobi::Cancelled`: the run was stopped through a `Yobi::Cancellation` token (see ["Cancelling a running command"](#cancelling-a-running-command) below).
|
|
219
224
|
|
|
220
225
|
Every method below lives on `Yobi::Repository` unless noted otherwise.
|
|
221
226
|
|
|
@@ -230,7 +235,7 @@ repo.init
|
|
|
230
235
|
# => #<Yobi::Initialized id="..." repository="...">
|
|
231
236
|
```
|
|
232
237
|
|
|
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 [
|
|
238
|
+
`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.1.0/Yobi/Repository#init-instance_method) for the full list of options. Returns an `Initialized` (`#id`/`#repository`).
|
|
234
239
|
|
|
235
240
|
### `#cat_config`
|
|
236
241
|
|
|
@@ -261,7 +266,7 @@ outcome.summary["data_added"]
|
|
|
261
266
|
|
|
262
267
|
`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
268
|
|
|
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 [
|
|
269
|
+
`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.1.0/Yobi/Repository#backup-instance_method) for the full list of options (filtering, retention, timing, and more).
|
|
265
270
|
|
|
266
271
|
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
272
|
|
|
@@ -291,7 +296,7 @@ outcome.summary["snapshot_id"]
|
|
|
291
296
|
repo.restore(snapshot_id: "latest", target: "/tmp/restore")
|
|
292
297
|
```
|
|
293
298
|
|
|
294
|
-
`snapshot_id:` accepts `"latest"` or a real ID; `target:` is the destination directory. `delete:` removes files in `target:` not present in the snapshot -
|
|
299
|
+
`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.1.0/Yobi/Repository#restore-instance_method) for the full list of options (filtering, `overwrite:` behavior, and more).
|
|
295
300
|
|
|
296
301
|
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
302
|
|
|
@@ -315,7 +320,7 @@ repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.tar")
|
|
|
315
320
|
repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.zip", archive: "zip")
|
|
316
321
|
```
|
|
317
322
|
|
|
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 [
|
|
323
|
+
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.1.0/Yobi/Repository#dump-instance_method) for the full list of options.
|
|
319
324
|
|
|
320
325
|
```ruby
|
|
321
326
|
handle = repo.dump(snapshot_id: "latest", file: "/var/www")
|
|
@@ -335,7 +340,7 @@ outcome.statistics.changed_files
|
|
|
335
340
|
outcome.changes.each { |change| puts "#{change.modifier} #{change.path}" }
|
|
336
341
|
```
|
|
337
342
|
|
|
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 [
|
|
343
|
+
`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.1.0/Yobi/Repository#diff-instance_method) for the full list of options.
|
|
339
344
|
|
|
340
345
|
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
346
|
|
|
@@ -349,7 +354,7 @@ repo.snapshots(tags: ["daily"], hosts: "web-1").each do |snapshot|
|
|
|
349
354
|
end
|
|
350
355
|
```
|
|
351
356
|
|
|
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 [
|
|
357
|
+
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.1.0/Yobi/Repository#snapshots-instance_method) for the full list of options.
|
|
353
358
|
|
|
354
359
|
### `#tag`
|
|
355
360
|
|
|
@@ -357,7 +362,7 @@ Returns an Enumerable of `Snapshot` (`#id`, `#short_id`, `#time`, `#host`, `#tag
|
|
|
357
362
|
repo.tag(snapshot_ids: snapshot.id, add: "verified")
|
|
358
363
|
```
|
|
359
364
|
|
|
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 [
|
|
365
|
+
`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.1.0/Yobi/Repository#tag-instance_method) for the full list of options.
|
|
361
366
|
|
|
362
367
|
Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#changed_snapshots`) and lazy `#changes` (Enumerable of `TagChange`, the old-ID/new-ID pairs).
|
|
363
368
|
|
|
@@ -367,7 +372,7 @@ Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#c
|
|
|
367
372
|
repo.forget(keep_daily: 7, keep_weekly: 4, keep_monthly: 12, prune: true)
|
|
368
373
|
```
|
|
369
374
|
|
|
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 [
|
|
375
|
+
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.1.0/Yobi/Repository#forget-instance_method) for the full list of options.
|
|
371
376
|
|
|
372
377
|
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
378
|
|
|
@@ -380,9 +385,9 @@ repo.find(patterns: "*.pem").each do |matches|
|
|
|
380
385
|
end
|
|
381
386
|
```
|
|
382
387
|
|
|
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 [
|
|
388
|
+
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.1.0/Yobi/Repository#find-instance_method) for the full list of options.
|
|
384
389
|
|
|
385
|
-
Returns an Enumerable of `MatchesPerSnapshot
|
|
390
|
+
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
391
|
|
|
387
392
|
### `#ls`
|
|
388
393
|
|
|
@@ -392,7 +397,7 @@ outcome.snapshot.short_id
|
|
|
392
397
|
outcome.nodes.each { |node| puts node.path }
|
|
393
398
|
```
|
|
394
399
|
|
|
395
|
-
Lists a snapshot's files/directories. `recursive:` descends into subdirectories. See the [
|
|
400
|
+
Lists a snapshot's files/directories. `recursive:` descends into subdirectories. See the [docs](https://rubydoc.info/gems/yobi/1.1.0/Yobi/Repository#ls-instance_method) for the full list of options.
|
|
396
401
|
|
|
397
402
|
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
403
|
|
|
@@ -406,7 +411,7 @@ outcome.summary.num_errors
|
|
|
406
411
|
outcome.errors.each { |error| puts error.message }
|
|
407
412
|
```
|
|
408
413
|
|
|
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 [
|
|
414
|
+
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.1.0/Yobi/Repository#check-instance_method) for the full list of options.
|
|
410
415
|
|
|
411
416
|
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
417
|
|
|
@@ -416,7 +421,7 @@ Returns a `CheckOutcome`: `#summary` (aliased `#report`; a `CheckSummary` coveri
|
|
|
416
421
|
repo.prune(max_unused: "5%")
|
|
417
422
|
```
|
|
418
423
|
|
|
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 [
|
|
424
|
+
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.1.0/Yobi/Repository#prune-instance_method) for the full list of options. Returns `true`.
|
|
420
425
|
|
|
421
426
|
### `#repair_index`, `#repair_packs`, `#repair_snapshots`
|
|
422
427
|
|
|
@@ -426,7 +431,7 @@ repo.repair_packs(ids: damaged_pack_ids)
|
|
|
426
431
|
repo.repair_snapshots(snapshot_ids: affected_ids, forget: true)
|
|
427
432
|
```
|
|
428
433
|
|
|
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
|
|
434
|
+
`#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.1.0/Yobi/Repository#repair_snapshots-instance_method) for the full list of options.
|
|
430
435
|
|
|
431
436
|
### `#recover`
|
|
432
437
|
|
|
@@ -434,7 +439,7 @@ repo.repair_snapshots(snapshot_ids: affected_ids, forget: true)
|
|
|
434
439
|
repo.recover
|
|
435
440
|
```
|
|
436
441
|
|
|
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.
|
|
442
|
+
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
443
|
|
|
439
444
|
### `#rewrite`
|
|
440
445
|
|
|
@@ -442,7 +447,7 @@ Builds a new snapshot from any data present in the repository but not referenced
|
|
|
442
447
|
repo.rewrite(snapshot_ids: old.id, excludes: "*.log", new_time: Time.now.iso8601)
|
|
443
448
|
```
|
|
444
449
|
|
|
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
|
|
450
|
+
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.1.0/Yobi/Repository#rewrite-instance_method) for the full list of options.
|
|
446
451
|
|
|
447
452
|
### `#migrate`
|
|
448
453
|
|
|
@@ -467,7 +472,7 @@ repo.stats(mode: "raw-data")
|
|
|
467
472
|
# => #<Yobi::RepositoryStats total_size=... total_file_count=... snapshots_count=...>
|
|
468
473
|
```
|
|
469
474
|
|
|
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 [
|
|
475
|
+
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.1.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
476
|
|
|
472
477
|
## Key management
|
|
473
478
|
|
|
@@ -480,9 +485,9 @@ repo.key_remove(id: old_key.id)
|
|
|
480
485
|
|
|
481
486
|
`#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
487
|
|
|
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 [
|
|
488
|
+
`#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.1.0/Yobi/Repository#key_add-instance_method) for the full list of options.
|
|
484
489
|
|
|
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
|
|
490
|
+
`#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
491
|
|
|
487
492
|
```ruby
|
|
488
493
|
repo.key_passwd(new_password: "new-password-value")
|
|
@@ -503,7 +508,7 @@ repo.list(:packs) # => Array of pack IDs
|
|
|
503
508
|
|
|
504
509
|
`#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
510
|
|
|
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;
|
|
511
|
+
`#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
512
|
|
|
508
513
|
## Across repositories
|
|
509
514
|
|
|
@@ -521,7 +526,7 @@ A shortcut for setting up a `#copy` destination: constructs a new `Repository` a
|
|
|
521
526
|
mirror_repo.copy(from_repo: primary_repo, tags: "nightly")
|
|
522
527
|
```
|
|
523
528
|
|
|
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 [
|
|
529
|
+
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.1.0/Yobi/Repository#copy-instance_method) for the full list of options. Returns `true`.
|
|
525
530
|
|
|
526
531
|
## Mounting
|
|
527
532
|
|
|
@@ -544,7 +549,7 @@ ensure
|
|
|
544
549
|
end
|
|
545
550
|
```
|
|
546
551
|
|
|
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 [
|
|
552
|
+
`#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.1.0/Yobi/Repository#mount-instance_method) for the full list of options.
|
|
548
553
|
|
|
549
554
|
## Restic-level (not scoped to a repository)
|
|
550
555
|
|
|
@@ -554,11 +559,11 @@ restic.version.version # => "0.19.1"
|
|
|
554
559
|
restic.cache(cleanup: true)
|
|
555
560
|
```
|
|
556
561
|
|
|
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 [
|
|
562
|
+
`#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.1.0/Yobi/Restic#cache-instance_method) for its remaining options.
|
|
558
563
|
|
|
559
564
|
## Global `Restic` settings
|
|
560
565
|
|
|
561
|
-
|
|
566
|
+
`Yobi::Restic` holds settings that apply to every command, regardless of repository:
|
|
562
567
|
|
|
563
568
|
```ruby
|
|
564
569
|
restic = Yobi::Restic.new(
|
|
@@ -568,14 +573,9 @@ restic = Yobi::Restic.new(
|
|
|
568
573
|
)
|
|
569
574
|
```
|
|
570
575
|
|
|
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`).
|
|
576
|
+
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
577
|
|
|
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.
|
|
578
|
+
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.1.0/Yobi/Restic).
|
|
579
579
|
|
|
580
580
|
### Sharing a `Restic` across repositories
|
|
581
581
|
|
|
@@ -586,11 +586,50 @@ customer_a = Yobi::Repository.new(url: "s3:.../customer-a", password: "...", res
|
|
|
586
586
|
customer_b = Yobi::Repository.new(url: "s3:.../customer-b", password: "...", restic: restic)
|
|
587
587
|
```
|
|
588
588
|
|
|
589
|
-
Both
|
|
589
|
+
Both use the same settings. Changing `restic.limit_upload` later affects every repository built from it.
|
|
590
|
+
|
|
591
|
+
`--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.
|
|
592
|
+
|
|
593
|
+
## Cancelling a running command
|
|
594
|
+
|
|
595
|
+
Call `Repository#with_cancellation(token)` before `#backup`, `#restore`, `#check`, `#prune`, `#forget`, or `#copy`, then `token.cancel!` from another thread to stop it:
|
|
596
|
+
|
|
597
|
+
```ruby
|
|
598
|
+
token = Yobi::Cancellation.new
|
|
599
|
+
|
|
600
|
+
worker = Thread.new do
|
|
601
|
+
repo.with_cancellation(token).backup(source: "/Users/zia/Documents")
|
|
602
|
+
rescue Yobi::Cancelled
|
|
603
|
+
# the user stopped it; not a backup failure
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
token.cancel! # => true
|
|
607
|
+
worker.join
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`#cancel!` sends **SIGINT**, not SIGKILL - Restic removes its own repository lock on the way out, so no `#unlock` is needed. Override with `cancel!(signal: "TERM")` only if you have a reason to.
|
|
611
|
+
|
|
612
|
+
Restic exits with its own documented code 130 when killed by a signal; Yobi raises `Yobi::Cancelled` for that instead of `Yobi::ResticCommandFailed`.
|
|
613
|
+
|
|
614
|
+
Safe at any point, from any thread; `#cancel!` is idempotent, returning `true` the first time and `false` after. Cancelling before the command starts means Restic never spawns; `Yobi::Cancelled#before_start?` tells the two cases apart:
|
|
615
|
+
|
|
616
|
+
```ruby
|
|
617
|
+
begin
|
|
618
|
+
repo.with_cancellation(token).backup(source: "/data")
|
|
619
|
+
rescue Yobi::Cancelled => error
|
|
620
|
+
error.before_start? # => true if Restic never ran
|
|
621
|
+
error.execution[:exit_code] # => nil in that case, otherwise the real exit code
|
|
622
|
+
end
|
|
623
|
+
```
|
|
590
624
|
|
|
591
|
-
|
|
625
|
+
What an interrupted run leaves behind:
|
|
592
626
|
|
|
593
|
-
|
|
627
|
+
- `#backup`: no snapshot is written; data already uploaded is generally left *unreferenced* rather than reused by the next run, and orphaned packs are only reclaimed by a later `#prune`.
|
|
628
|
+
- `#restore`: the target directory may be left partially written (some files complete, others missing).
|
|
629
|
+
- `#check`: read-only, nothing is written; safe to cancel at any point.
|
|
630
|
+
- `#prune`: Restic's repacking is designed to be crash-safe, but repacking done so far may not be reused (the next `#prune` can end up starting over).
|
|
631
|
+
- `#forget`: some snapshots may already be removed while the rest are intact, re-run to finish.
|
|
632
|
+
- `#copy`: snapshots already copied should stay in the destination and be skipped on a re-run.
|
|
594
633
|
|
|
595
634
|
## Redaction
|
|
596
635
|
|
|
@@ -601,7 +640,7 @@ repo.inspect
|
|
|
601
640
|
# => #<Yobi::Repository url="s3:..." password="[FILTERED]" backend_credentials={"AWS_ACCESS_KEY_ID"=>"[FILTERED]", "AWS_SECRET_ACCESS_KEY"=>"[FILTERED]"}>
|
|
602
641
|
```
|
|
603
642
|
|
|
604
|
-
A literal or command/file-sourced `password:` shows as `"[FILTERED]"`; anything callable shows as `"[RESOLVER]"` **without ever being called**.
|
|
643
|
+
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
644
|
|
|
606
645
|
`#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
646
|
|
|
@@ -636,7 +675,7 @@ repo.forget(
|
|
|
636
675
|
)
|
|
637
676
|
```
|
|
638
677
|
|
|
639
|
-
A non-empty `outcome.errors` (exit code 3) means some files were skipped but the snapshot was still created
|
|
678
|
+
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
679
|
|
|
641
680
|
### Restore workflows
|
|
642
681
|
|
|
@@ -704,7 +743,7 @@ password: -> {
|
|
|
704
743
|
}
|
|
705
744
|
```
|
|
706
745
|
|
|
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
|
|
746
|
+
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
747
|
|
|
709
748
|
### Database dumps via `stdin_from_command`
|
|
710
749
|
|
|
@@ -717,7 +756,7 @@ repo.backup(
|
|
|
717
756
|
)
|
|
718
757
|
```
|
|
719
758
|
|
|
720
|
-
The third element (`"mydb.sql"`) names the resulting virtual file inside the snapshot. Omit it
|
|
759
|
+
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
760
|
|
|
722
761
|
```ruby
|
|
723
762
|
repo.backup(source: [:stdin_from_command, ["mysqldump", "-u", "backup", "my app db"]])
|
|
@@ -772,7 +811,7 @@ Customer.find_each do |customer|
|
|
|
772
811
|
end
|
|
773
812
|
```
|
|
774
813
|
|
|
775
|
-
|
|
814
|
+
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
815
|
|
|
777
816
|
```ruby
|
|
778
817
|
queue = Queue.new
|
|
@@ -836,7 +875,7 @@ end
|
|
|
836
875
|
with_retry { repo.backup(source: source_path) }
|
|
837
876
|
```
|
|
838
877
|
|
|
839
|
-
For a one-off manual fix
|
|
878
|
+
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
879
|
|
|
841
880
|
## Non-goals
|
|
842
881
|
|
|
@@ -855,7 +894,7 @@ After checking out the repo, run `bin/setup` to install dependencies.
|
|
|
855
894
|
- `bin/test`: both, in parallel.
|
|
856
895
|
- `bin/standardrb`: lint.
|
|
857
896
|
- `bin/console`: an interactive prompt with the gem loaded.
|
|
858
|
-
- `bin/docs`:
|
|
897
|
+
- `bin/docs`: regenerates RDoc into `./doc` and opens the index in the default browser.
|
|
859
898
|
|
|
860
899
|
See `MAINTAINERS.md` for what to keep in sync as the API changes, and the release checklist.
|
|
861
900
|
|
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
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Yobi
|
|
2
|
+
# Returned by Repository#with_cancellation. Exposes only the six
|
|
3
|
+
# long-running methods a Yobi::Cancellation token can stop; every other
|
|
4
|
+
# Repository method (#snapshots, #mount, #init, ...) is unreachable here.
|
|
5
|
+
#
|
|
6
|
+
# +token+ reaches the repository via a thread-local, set only for the
|
|
7
|
+
# duration of each call and only on the calling thread.
|
|
8
|
+
class CancellableProxy
|
|
9
|
+
def initialize(repository, token)
|
|
10
|
+
@repository = repository
|
|
11
|
+
@token = token
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def backup(...)
|
|
15
|
+
with_token { @repository.backup(...) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def restore(...)
|
|
19
|
+
with_token { @repository.restore(...) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def check(...)
|
|
23
|
+
with_token { @repository.check(...) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prune(...)
|
|
27
|
+
with_token { @repository.prune(...) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def forget(...)
|
|
31
|
+
with_token { @repository.forget(...) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def copy(...)
|
|
35
|
+
with_token { @repository.copy(...) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def with_token
|
|
41
|
+
previous = Thread.current[:yobi_cancellation]
|
|
42
|
+
Thread.current[:yobi_cancellation] = @token
|
|
43
|
+
yield
|
|
44
|
+
ensure
|
|
45
|
+
Thread.current[:yobi_cancellation] = previous
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|