yobi 1.0.0 → 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/CHANGELOG.md +6 -0
- data/README.md +62 -20
- data/lib/yobi/cancellable_proxy.rb +48 -0
- data/lib/yobi/cancellation.rb +97 -0
- data/lib/yobi/errors.rb +27 -0
- data/lib/yobi/repository.rb +19 -1
- data/lib/yobi/restic.rb +22 -6
- data/lib/yobi/version.rb +1 -1
- data/lib/yobi.rb +2 -0
- data/sig/yobi.rbs +79 -2
- metadata +4 -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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## [1.0.0] - 2026-08-29
|
|
2
8
|
|
|
3
9
|
### Changed
|
data/README.md
CHANGED
|
@@ -220,6 +220,7 @@ The full hierarchy, all under `Yobi::Error < StandardError`:
|
|
|
220
220
|
- `Yobi::RepositoryNotFound`, `Yobi::RepositoryLocked`, `Yobi::AuthenticationFailed`: Restic's own typed exit codes (10/11/12).
|
|
221
221
|
- `Yobi::ResticCommandFailed`: any other non-zero exit, for a failure that doesn't fit one of the above.
|
|
222
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).
|
|
223
224
|
|
|
224
225
|
Every method below lives on `Yobi::Repository` unless noted otherwise.
|
|
225
226
|
|
|
@@ -234,7 +235,7 @@ repo.init
|
|
|
234
235
|
# => #<Yobi::Initialized id="..." repository="...">
|
|
235
236
|
```
|
|
236
237
|
|
|
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.
|
|
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`).
|
|
238
239
|
|
|
239
240
|
### `#cat_config`
|
|
240
241
|
|
|
@@ -265,7 +266,7 @@ outcome.summary["data_added"]
|
|
|
265
266
|
|
|
266
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).
|
|
267
268
|
|
|
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.
|
|
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).
|
|
269
270
|
|
|
270
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).
|
|
271
272
|
|
|
@@ -295,7 +296,7 @@ outcome.summary["snapshot_id"]
|
|
|
295
296
|
repo.restore(snapshot_id: "latest", target: "/tmp/restore")
|
|
296
297
|
```
|
|
297
298
|
|
|
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.
|
|
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).
|
|
299
300
|
|
|
300
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.
|
|
301
302
|
|
|
@@ -319,7 +320,7 @@ repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.tar")
|
|
|
319
320
|
repo.dump(snapshot_id: "latest", file: "/var/www", target: "/tmp/www.zip", archive: "zip")
|
|
320
321
|
```
|
|
321
322
|
|
|
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.
|
|
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.
|
|
323
324
|
|
|
324
325
|
```ruby
|
|
325
326
|
handle = repo.dump(snapshot_id: "latest", file: "/var/www")
|
|
@@ -339,7 +340,7 @@ outcome.statistics.changed_files
|
|
|
339
340
|
outcome.changes.each { |change| puts "#{change.modifier} #{change.path}" }
|
|
340
341
|
```
|
|
341
342
|
|
|
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.
|
|
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.
|
|
343
344
|
|
|
344
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`).
|
|
345
346
|
|
|
@@ -353,7 +354,7 @@ repo.snapshots(tags: ["daily"], hosts: "web-1").each do |snapshot|
|
|
|
353
354
|
end
|
|
354
355
|
```
|
|
355
356
|
|
|
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.
|
|
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.
|
|
357
358
|
|
|
358
359
|
### `#tag`
|
|
359
360
|
|
|
@@ -361,7 +362,7 @@ Returns an Enumerable of `Snapshot` (`#id`, `#short_id`, `#time`, `#host`, `#tag
|
|
|
361
362
|
repo.tag(snapshot_ids: snapshot.id, add: "verified")
|
|
362
363
|
```
|
|
363
364
|
|
|
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.
|
|
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.
|
|
365
366
|
|
|
366
367
|
Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#changed_snapshots`) and lazy `#changes` (Enumerable of `TagChange`, the old-ID/new-ID pairs).
|
|
367
368
|
|
|
@@ -371,7 +372,7 @@ Returns a `TagOutcome`: `#summary` (aliased `#report`; a `TagSummary` - just `#c
|
|
|
371
372
|
repo.forget(keep_daily: 7, keep_weekly: 4, keep_monthly: 12, prune: true)
|
|
372
373
|
```
|
|
373
374
|
|
|
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.
|
|
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.
|
|
375
376
|
|
|
376
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"`).
|
|
377
378
|
|
|
@@ -384,7 +385,7 @@ repo.find(patterns: "*.pem").each do |matches|
|
|
|
384
385
|
end
|
|
385
386
|
```
|
|
386
387
|
|
|
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.
|
|
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.
|
|
388
389
|
|
|
389
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.).
|
|
390
391
|
|
|
@@ -396,7 +397,7 @@ outcome.snapshot.short_id
|
|
|
396
397
|
outcome.nodes.each { |node| puts node.path }
|
|
397
398
|
```
|
|
398
399
|
|
|
399
|
-
Lists a snapshot's files/directories. `recursive:` descends into subdirectories. See the [docs](https://rubydoc.info/gems/yobi/1.
|
|
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.
|
|
400
401
|
|
|
401
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).
|
|
402
403
|
|
|
@@ -410,7 +411,7 @@ outcome.summary.num_errors
|
|
|
410
411
|
outcome.errors.each { |error| puts error.message }
|
|
411
412
|
```
|
|
412
413
|
|
|
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.
|
|
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.
|
|
414
415
|
|
|
415
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.
|
|
416
417
|
|
|
@@ -420,7 +421,7 @@ Returns a `CheckOutcome`: `#summary` (aliased `#report`; a `CheckSummary` coveri
|
|
|
420
421
|
repo.prune(max_unused: "5%")
|
|
421
422
|
```
|
|
422
423
|
|
|
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.
|
|
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`.
|
|
424
425
|
|
|
425
426
|
### `#repair_index`, `#repair_packs`, `#repair_snapshots`
|
|
426
427
|
|
|
@@ -430,7 +431,7 @@ repo.repair_packs(ids: damaged_pack_ids)
|
|
|
430
431
|
repo.repair_snapshots(snapshot_ids: affected_ids, forget: true)
|
|
431
432
|
```
|
|
432
433
|
|
|
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.
|
|
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.
|
|
434
435
|
|
|
435
436
|
### `#recover`
|
|
436
437
|
|
|
@@ -446,7 +447,7 @@ Builds a new snapshot from any data present in the repository but not referenced
|
|
|
446
447
|
repo.rewrite(snapshot_ids: old.id, excludes: "*.log", new_time: Time.now.iso8601)
|
|
447
448
|
```
|
|
448
449
|
|
|
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.
|
|
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.
|
|
450
451
|
|
|
451
452
|
### `#migrate`
|
|
452
453
|
|
|
@@ -471,7 +472,7 @@ repo.stats(mode: "raw-data")
|
|
|
471
472
|
# => #<Yobi::RepositoryStats total_size=... total_file_count=... snapshots_count=...>
|
|
472
473
|
```
|
|
473
474
|
|
|
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.
|
|
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.
|
|
475
476
|
|
|
476
477
|
## Key management
|
|
477
478
|
|
|
@@ -484,7 +485,7 @@ repo.key_remove(id: old_key.id)
|
|
|
484
485
|
|
|
485
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`).
|
|
486
487
|
|
|
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.
|
|
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.
|
|
488
489
|
|
|
489
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:
|
|
490
491
|
|
|
@@ -525,7 +526,7 @@ A shortcut for setting up a `#copy` destination: constructs a new `Repository` a
|
|
|
525
526
|
mirror_repo.copy(from_repo: primary_repo, tags: "nightly")
|
|
526
527
|
```
|
|
527
528
|
|
|
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.
|
|
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`.
|
|
529
530
|
|
|
530
531
|
## Mounting
|
|
531
532
|
|
|
@@ -548,7 +549,7 @@ ensure
|
|
|
548
549
|
end
|
|
549
550
|
```
|
|
550
551
|
|
|
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.
|
|
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.
|
|
552
553
|
|
|
553
554
|
## Restic-level (not scoped to a repository)
|
|
554
555
|
|
|
@@ -558,7 +559,7 @@ restic.version.version # => "0.19.1"
|
|
|
558
559
|
restic.cache(cleanup: true)
|
|
559
560
|
```
|
|
560
561
|
|
|
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.
|
|
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.
|
|
562
563
|
|
|
563
564
|
## Global `Restic` settings
|
|
564
565
|
|
|
@@ -574,7 +575,7 @@ restic = Yobi::Restic.new(
|
|
|
574
575
|
|
|
575
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`.
|
|
576
577
|
|
|
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.
|
|
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).
|
|
578
579
|
|
|
579
580
|
### Sharing a `Restic` across repositories
|
|
580
581
|
|
|
@@ -589,6 +590,47 @@ Both use the same settings. Changing `restic.limit_upload` later affects every r
|
|
|
589
590
|
|
|
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.
|
|
591
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
|
+
```
|
|
624
|
+
|
|
625
|
+
What an interrupted run leaves behind:
|
|
626
|
+
|
|
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.
|
|
633
|
+
|
|
592
634
|
## Redaction
|
|
593
635
|
|
|
594
636
|
Both `Repository#inspect` and `Restic#inspect` redact sensitive values, so a stray `pp`/`puts`/log call never prints a credential in plaintext:
|
|
@@ -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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yobi
|
|
4
|
+
# A cancellation token for a long-running Restic run.
|
|
5
|
+
#
|
|
6
|
+
# Pass one to Yobi::Repository#with_cancellation, then call #cancel! from
|
|
7
|
+
# another thread to stop whichever long-running method (#backup,
|
|
8
|
+
# #restore, #check, #prune, #forget, #copy) is running on the repository
|
|
9
|
+
# it returns:
|
|
10
|
+
#
|
|
11
|
+
# token = Yobi::Cancellation.new
|
|
12
|
+
# Thread.new { token.cancel! if user_clicked_cancel }
|
|
13
|
+
# repo.with_cancellation(token).backup(source: "/data")
|
|
14
|
+
# # => raises Yobi::Cancelled
|
|
15
|
+
#
|
|
16
|
+
# #cancel! sends the Restic process SIGINT rather than SIGKILL, so Restic
|
|
17
|
+
# removes its own repository lock on the way out and no +restic unlock+ is
|
|
18
|
+
# needed afterwards. An interrupted +backup+ writes no snapshot; data it had
|
|
19
|
+
# already uploaded is generally left unreferenced until the next +prune+
|
|
20
|
+
# rather than being reused, so cancelling does discard in-progress work.
|
|
21
|
+
#
|
|
22
|
+
# Safe to call #cancel! before the run starts (the run then raises without
|
|
23
|
+
# spawning Restic at all), after it has finished (a no-op), and from any
|
|
24
|
+
# thread. A token tracks one run at a time and is not reusable once
|
|
25
|
+
# cancelled.
|
|
26
|
+
class Cancellation
|
|
27
|
+
# The signal sent to Restic by #cancel! unless overridden.
|
|
28
|
+
DEFAULT_SIGNAL = "INT"
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@cancelled = false
|
|
33
|
+
@signal = DEFAULT_SIGNAL
|
|
34
|
+
@pid = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# True once #cancel! has been called.
|
|
38
|
+
def cancelled?
|
|
39
|
+
@mutex.synchronize { @cancelled }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Requests cancellation, signalling the attached Restic process if one is
|
|
43
|
+
# currently running. Returns true the first time, false if this token was
|
|
44
|
+
# already cancelled. Idempotent and thread-safe.
|
|
45
|
+
#
|
|
46
|
+
# +signal:+ overrides the signal sent; the SIGINT default is what lets
|
|
47
|
+
# Restic clean up its lock, so override it only when you have a reason to.
|
|
48
|
+
def cancel!(signal: DEFAULT_SIGNAL)
|
|
49
|
+
pid = nil
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
return false if @cancelled
|
|
52
|
+
|
|
53
|
+
@cancelled = true
|
|
54
|
+
@signal = signal
|
|
55
|
+
pid = @pid
|
|
56
|
+
end
|
|
57
|
+
signal_pid(pid, signal)
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Attaches a freshly spawned Restic process. If #cancel! already landed
|
|
62
|
+
# before the spawn, the process is signalled immediately. Called by
|
|
63
|
+
# Yobi::Restic; not part of the public API.
|
|
64
|
+
def attach(pid) # :nodoc:
|
|
65
|
+
cancelled, signal = @mutex.synchronize do
|
|
66
|
+
@pid = pid
|
|
67
|
+
[@cancelled, @signal]
|
|
68
|
+
end
|
|
69
|
+
signal_pid(pid, signal) if cancelled
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Detaches the current process once it has been reaped, so a later
|
|
73
|
+
# #cancel! can't signal a recycled PID. Called by Yobi::Restic; not part
|
|
74
|
+
# of the public API.
|
|
75
|
+
def detach # :nodoc:
|
|
76
|
+
@mutex.synchronize { @pid = nil }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect
|
|
80
|
+
"#<#{self.class} cancelled=#{cancelled?}>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Errno::ESRCH means Restic already exited between the cancel and the
|
|
86
|
+
# signal, which is exactly the outcome cancelling wanted.
|
|
87
|
+
def signal_pid(pid, signal)
|
|
88
|
+
return if pid.nil?
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
Process.kill(signal, pid)
|
|
92
|
+
rescue Errno::ESRCH
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/yobi/errors.rb
CHANGED
|
@@ -41,6 +41,33 @@ module Yobi
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Raised when a run was stopped through a Yobi::Cancellation token.
|
|
45
|
+
#
|
|
46
|
+
# Distinct from Yobi::ResticExecutionError and its subclasses: the run did
|
|
47
|
+
# not fail, the caller stopped it on purpose. Rescue this to record a
|
|
48
|
+
# cancellation without treating it as a backup failure.
|
|
49
|
+
class Cancelled < Error
|
|
50
|
+
# The raw +{exit_code:, output:, argv:}+ result. +exit_code:+ and
|
|
51
|
+
# +output:+ are +nil+ when the token was already cancelled before
|
|
52
|
+
# Restic was spawned at all.
|
|
53
|
+
attr_reader :execution
|
|
54
|
+
|
|
55
|
+
def initialize(execution) # :nodoc:
|
|
56
|
+
@execution = execution
|
|
57
|
+
super("Restic run was cancelled")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The argv of the run that was cancelled.
|
|
61
|
+
def argv
|
|
62
|
+
execution[:argv]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# True when the token was cancelled before Restic even started.
|
|
66
|
+
def before_start?
|
|
67
|
+
execution[:exit_code].nil?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
44
71
|
# Raised when the Restic binary itself can't be found or executed.
|
|
45
72
|
class ResticNotFound < Error
|
|
46
73
|
attr_reader :restic_path, :argv
|
data/lib/yobi/repository.rb
CHANGED
|
@@ -73,10 +73,28 @@ module Yobi
|
|
|
73
73
|
"#<#{self.class} url=#{url.inspect} password=#{redacted_password.inspect} backend_credentials=#{redacted_backend_credentials.inspect}>"
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Returns a Yobi::CancellableProxy wrapping this repository, exposing
|
|
77
|
+
# only its long-running methods (#backup, #restore, #check, #prune,
|
|
78
|
+
# #forget, #copy) with +token+ threaded through them, so #cancel! can
|
|
79
|
+
# stop whichever one is called:
|
|
80
|
+
#
|
|
81
|
+
# token = Yobi::Cancellation.new
|
|
82
|
+
# Thread.new { token.cancel! if user_clicked_cancel }
|
|
83
|
+
# repo.with_cancellation(token).backup(source: "/data")
|
|
84
|
+
def with_cancellation(token)
|
|
85
|
+
CancellableProxy.new(self, token)
|
|
86
|
+
end
|
|
87
|
+
|
|
76
88
|
private
|
|
77
89
|
|
|
90
|
+
# Set for the duration of one call by a Yobi::CancellableProxy, on the
|
|
91
|
+
# calling thread only.
|
|
92
|
+
def cancellation
|
|
93
|
+
Thread.current[:yobi_cancellation]
|
|
94
|
+
end
|
|
95
|
+
|
|
78
96
|
def run_restic(argv, extra_env: {}, output: nil, &block)
|
|
79
|
-
@restic.run(argv, extra_env: env.merge(extra_env), output: output, &block)
|
|
97
|
+
@restic.run(argv, extra_env: env.merge(extra_env), output: output, cancellation: cancellation, &block)
|
|
80
98
|
end
|
|
81
99
|
|
|
82
100
|
def build_argv(*base)
|
data/lib/yobi/restic.rb
CHANGED
|
@@ -169,14 +169,22 @@ module Yobi
|
|
|
169
169
|
#
|
|
170
170
|
# Returns +{exit_code:, output:, argv:}+ on exit code 0 or 3.
|
|
171
171
|
# Raises Yobi::RepositoryNotFound, Yobi::RepositoryLocked,
|
|
172
|
-
# Yobi::AuthenticationFailed,
|
|
173
|
-
|
|
172
|
+
# Yobi::AuthenticationFailed, Yobi::Cancelled (exit code 130, e.g. from
|
|
173
|
+
# a Yobi::Cancellation token's #cancel!), or Yobi::ResticCommandFailed
|
|
174
|
+
# otherwise.
|
|
175
|
+
def run(argv, extra_env: {}, skip_version_check: false, output: nil, cancellation: nil, &block) # :nodoc:
|
|
174
176
|
ensure_minimum_version! unless skip_version_check
|
|
177
|
+
raise Yobi::Cancelled.new({exit_code: nil, output: nil, argv: argv}) if cancellation&.cancelled?
|
|
178
|
+
|
|
179
|
+
# Only forwarded when a token was actually supplied, so the common
|
|
180
|
+
# short metadata commands keep their original call shape.
|
|
181
|
+
cancel_kwarg = cancellation ? {cancellation: cancellation} : {}
|
|
175
182
|
execution = if output || block
|
|
176
|
-
execute_with_streaming(argv, extra_env, output: output, &block)
|
|
183
|
+
execute_with_streaming(argv, extra_env, output: output, **cancel_kwarg, &block)
|
|
177
184
|
else
|
|
178
|
-
execute(argv, extra_env)
|
|
185
|
+
execute(argv, extra_env, **cancel_kwarg)
|
|
179
186
|
end
|
|
187
|
+
|
|
180
188
|
self.class.dispatch(execution)
|
|
181
189
|
end
|
|
182
190
|
|
|
@@ -192,6 +200,8 @@ module Yobi
|
|
|
192
200
|
raise Yobi::RepositoryLocked, execution
|
|
193
201
|
when 12
|
|
194
202
|
raise Yobi::AuthenticationFailed, execution
|
|
203
|
+
when 130
|
|
204
|
+
raise Yobi::Cancelled, execution
|
|
195
205
|
else
|
|
196
206
|
raise Yobi::ResticCommandFailed, execution
|
|
197
207
|
end
|
|
@@ -281,21 +291,25 @@ module Yobi
|
|
|
281
291
|
end
|
|
282
292
|
end
|
|
283
293
|
|
|
284
|
-
def execute(argv, extra_env)
|
|
294
|
+
def execute(argv, extra_env, cancellation: nil)
|
|
285
295
|
output = Yobi::ResticOutput.new
|
|
286
296
|
pid = Process.spawn(env.merge(extra_env), restic_path, *argv, out: output.file, err: output.file)
|
|
297
|
+
cancellation&.attach(pid)
|
|
287
298
|
_, status = Process.wait2(pid)
|
|
288
299
|
{exit_code: status.exitstatus, output: output, argv: argv}
|
|
289
300
|
rescue Errno::ENOENT
|
|
290
301
|
output.file.close
|
|
291
302
|
raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
|
|
303
|
+
ensure
|
|
304
|
+
cancellation&.detach
|
|
292
305
|
end
|
|
293
306
|
|
|
294
|
-
def execute_with_streaming(argv, extra_env, output: nil)
|
|
307
|
+
def execute_with_streaming(argv, extra_env, output: nil, cancellation: nil)
|
|
295
308
|
output ||= Yobi::ResticOutput.new
|
|
296
309
|
|
|
297
310
|
Open3.popen3(env.merge(extra_env), restic_path, *argv) do |stdin, stdout, stderr, wait_thr|
|
|
298
311
|
stdin.close
|
|
312
|
+
cancellation&.attach(wait_thr.pid)
|
|
299
313
|
readers = {stdout => :stdout, stderr => :stderr}
|
|
300
314
|
|
|
301
315
|
until readers.empty?
|
|
@@ -317,6 +331,8 @@ module Yobi
|
|
|
317
331
|
rescue Errno::ENOENT
|
|
318
332
|
output.file.close
|
|
319
333
|
raise Yobi::ResticNotFound.new(restic_path: restic_path, argv: argv)
|
|
334
|
+
ensure
|
|
335
|
+
cancellation&.detach
|
|
320
336
|
end
|
|
321
337
|
end
|
|
322
338
|
|
data/lib/yobi/version.rb
CHANGED
data/lib/yobi.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "yobi/version"
|
|
4
4
|
require_relative "yobi/fancy_hash"
|
|
5
|
+
require_relative "yobi/cancellation"
|
|
5
6
|
require_relative "yobi/errors"
|
|
6
7
|
require_relative "yobi/argv_builder"
|
|
7
8
|
require_relative "yobi/restic_output"
|
|
@@ -10,5 +11,6 @@ require_relative "yobi/mount_handle"
|
|
|
10
11
|
require_relative "yobi/restic"
|
|
11
12
|
require_relative "yobi/snapshot"
|
|
12
13
|
require_relative "yobi/repository"
|
|
14
|
+
require_relative "yobi/cancellable_proxy"
|
|
13
15
|
|
|
14
16
|
Dir[File.join(__dir__, "yobi", "repository", "*.rb")].sort.each { |file| require_relative file }
|
data/sig/yobi.rbs
CHANGED
|
@@ -31,6 +31,32 @@ module Yobi
|
|
|
31
31
|
def initialize: (installed_version: String, minimum_version: String) -> void
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
class Cancellation
|
|
35
|
+
DEFAULT_SIGNAL: String
|
|
36
|
+
|
|
37
|
+
def initialize: () -> void
|
|
38
|
+
|
|
39
|
+
def cancelled?: () -> bool
|
|
40
|
+
|
|
41
|
+
def cancel!: (?signal: String) -> bool
|
|
42
|
+
|
|
43
|
+
def attach: (Integer pid) -> void
|
|
44
|
+
|
|
45
|
+
def detach: () -> void
|
|
46
|
+
|
|
47
|
+
def inspect: () -> String
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Cancelled < Error
|
|
51
|
+
attr_reader execution: cancelled_execution
|
|
52
|
+
|
|
53
|
+
def initialize: (cancelled_execution execution) -> void
|
|
54
|
+
|
|
55
|
+
def argv: () -> Array[String]
|
|
56
|
+
|
|
57
|
+
def before_start?: () -> bool
|
|
58
|
+
end
|
|
59
|
+
|
|
34
60
|
class ResticNotFound < Error
|
|
35
61
|
attr_reader restic_path: String
|
|
36
62
|
attr_reader argv: Array[String]
|
|
@@ -39,6 +65,7 @@ module Yobi
|
|
|
39
65
|
end
|
|
40
66
|
|
|
41
67
|
type execution = { exit_code: Integer, output: ResticOutput, argv: Array[String] }
|
|
68
|
+
type cancelled_execution = { exit_code: Integer?, output: ResticOutput?, argv: Array[String] }
|
|
42
69
|
|
|
43
70
|
class ResticExecutionError < Error
|
|
44
71
|
attr_reader execution: execution
|
|
@@ -264,7 +291,7 @@ module Yobi
|
|
|
264
291
|
|
|
265
292
|
def cache: (?cleanup: bool, ?max_age: String?, ?no_size: bool) -> true
|
|
266
293
|
|
|
267
|
-
def run: (Array[String] argv, ?extra_env: Hash[String, String], ?skip_version_check: bool, ?output: ResticOutput?) ?{ (untyped message) -> void } -> execution
|
|
294
|
+
def run: (Array[String] argv, ?extra_env: Hash[String, String], ?skip_version_check: bool, ?output: ResticOutput?, ?cancellation: Cancellation?) ?{ (untyped message) -> void } -> execution
|
|
268
295
|
|
|
269
296
|
def self.dispatch: (execution execution) -> execution
|
|
270
297
|
|
|
@@ -305,6 +332,8 @@ module Yobi
|
|
|
305
332
|
|
|
306
333
|
alias config cat_config
|
|
307
334
|
|
|
335
|
+
def with_cancellation: (Cancellation token) -> CancellableProxy
|
|
336
|
+
|
|
308
337
|
def backup: (source: String | [:stdin_from_command, String | Array[String]] | [:stdin_from_command, String | Array[String], String],
|
|
309
338
|
?excludes: Array[String] | String, ?exclude_files: Array[String] | String,
|
|
310
339
|
?exclude_if_present: Array[String] | String, ?exclude_larger_than: String?,
|
|
@@ -418,7 +447,8 @@ module Yobi
|
|
|
418
447
|
?include_files: Array[String] | String, ?include_xattrs: Array[String] | String,
|
|
419
448
|
?paths: Array[String] | String, ?tags: Array[String] | String, ?delete: bool, ?dry_run: bool,
|
|
420
449
|
?overwrite: String | Symbol | nil, ?ownership_by_name: bool, ?sparse: bool, ?verbose: bool,
|
|
421
|
-
?verify: bool)
|
|
450
|
+
?verify: bool)
|
|
451
|
+
?{ (RestoreStatus | RestoreVerboseStatus message) -> void } -> RestoreOutcome
|
|
422
452
|
|
|
423
453
|
def rewrite: (?snapshot_ids: Array[String] | String, ?hosts: Array[String] | String,
|
|
424
454
|
?tags: Array[String] | String, ?paths: Array[String] | String, ?excludes: Array[String] | String,
|
|
@@ -441,6 +471,53 @@ module Yobi
|
|
|
441
471
|
def unlock: (?remove_all: bool) -> true
|
|
442
472
|
end
|
|
443
473
|
|
|
474
|
+
class CancellableProxy
|
|
475
|
+
def initialize: (Repository repository, Cancellation token) -> void
|
|
476
|
+
|
|
477
|
+
def backup: (source: String | [:stdin_from_command, String | Array[String]] | [:stdin_from_command, String | Array[String], String],
|
|
478
|
+
?excludes: Array[String] | String, ?exclude_files: Array[String] | String,
|
|
479
|
+
?exclude_if_present: Array[String] | String, ?exclude_larger_than: String?,
|
|
480
|
+
?files_from: Array[String] | String, ?files_from_raw: Array[String] | String,
|
|
481
|
+
?files_from_verbatim: Array[String] | String, ?iexcludes: Array[String] | String,
|
|
482
|
+
?iexclude_files: Array[String] | String, ?tags: Array[String] | String, ?dry_run: bool,
|
|
483
|
+
?exclude_caches: bool, ?exclude_cloud_files: bool, ?force: bool, ?group_by: String?,
|
|
484
|
+
?host: String?, ?ignore_ctime: bool, ?ignore_inode: bool, ?no_scan: bool,
|
|
485
|
+
?one_file_system: bool, ?parent: String?, ?read_concurrency: Integer?,
|
|
486
|
+
?skip_if_unchanged: bool, ?time: String?, ?verbose: bool, ?with_atime: bool)
|
|
487
|
+
?{ (BackupStatus | BackupError | BackupVerboseStatus | BackupSummary message) -> void } -> BackupOutcome
|
|
488
|
+
|
|
489
|
+
def restore: (snapshot_id: String, target: String, ?excludes: Array[String] | String,
|
|
490
|
+
?exclude_files: Array[String] | String, ?exclude_xattrs: Array[String] | String,
|
|
491
|
+
?hosts: Array[String] | String, ?iexcludes: Array[String] | String,
|
|
492
|
+
?iexclude_files: Array[String] | String, ?iincludes: Array[String] | String,
|
|
493
|
+
?iinclude_files: Array[String] | String, ?includes: Array[String] | String,
|
|
494
|
+
?include_files: Array[String] | String, ?include_xattrs: Array[String] | String,
|
|
495
|
+
?paths: Array[String] | String, ?tags: Array[String] | String, ?delete: bool, ?dry_run: bool,
|
|
496
|
+
?overwrite: String | Symbol | nil, ?ownership_by_name: bool, ?sparse: bool, ?verbose: bool,
|
|
497
|
+
?verify: bool)
|
|
498
|
+
?{ (RestoreStatus | RestoreVerboseStatus message) -> void } -> RestoreOutcome
|
|
499
|
+
|
|
500
|
+
def check: (?hosts: Array[String] | String, ?paths: Array[String] | String, ?read_data: bool,
|
|
501
|
+
?read_data_subset: String?, ?tags: Array[String] | String, ?with_cache: bool) -> CheckOutcome
|
|
502
|
+
|
|
503
|
+
def prune: (?dry_run: bool, ?max_repack_size: String?, ?max_unused: String?,
|
|
504
|
+
?repack_cacheable_only: bool, ?repack_smaller_than: String?, ?repack_uncompressed: bool,
|
|
505
|
+
?unsafe_recover_no_free_space: String?) -> true
|
|
506
|
+
|
|
507
|
+
def forget: (?keep_last: Integer?, ?keep_hourly: Integer?, ?keep_daily: Integer?, ?keep_weekly: Integer?,
|
|
508
|
+
?keep_monthly: Integer?, ?keep_yearly: Integer?, ?keep_within: String?, ?keep_within_hourly: String?,
|
|
509
|
+
?keep_within_daily: String?, ?keep_within_weekly: String?, ?keep_within_monthly: String?,
|
|
510
|
+
?keep_within_yearly: String?, ?keep_tags: Array[String] | String, ?hosts: Array[String] | String,
|
|
511
|
+
?tags: Array[String] | String, ?paths: Array[String] | String, ?compact: bool, ?group_by: String?,
|
|
512
|
+
?dry_run: bool, ?prune: bool, ?unsafe_allow_remove_all: bool, ?max_unused: String?,
|
|
513
|
+
?max_repack_size: String?, ?repack_cacheable_only: bool, ?repack_uncompressed: bool,
|
|
514
|
+
?repack_smaller_than: String?) -> Array[ForgetGroup]
|
|
515
|
+
|
|
516
|
+
def copy: (from_repo: from_repo, ?from_password: from_password, ?snapshot_ids: Array[String] | String,
|
|
517
|
+
?from_key_hint: String?, ?hosts: Array[String] | String,
|
|
518
|
+
?paths: Array[String] | String, ?tags: Array[String] | String) -> true
|
|
519
|
+
end
|
|
520
|
+
|
|
444
521
|
module BackupMessageWrapper
|
|
445
522
|
def self.call: (Hash[String, untyped] raw) -> (BackupStatus | BackupError | BackupVerboseStatus | BackupSummary | Hash[String, untyped])
|
|
446
523
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yobi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zia Perdana
|
|
@@ -22,6 +22,8 @@ files:
|
|
|
22
22
|
- README.md
|
|
23
23
|
- lib/yobi.rb
|
|
24
24
|
- lib/yobi/argv_builder.rb
|
|
25
|
+
- lib/yobi/cancellable_proxy.rb
|
|
26
|
+
- lib/yobi/cancellation.rb
|
|
25
27
|
- lib/yobi/errors.rb
|
|
26
28
|
- lib/yobi/fancy_hash.rb
|
|
27
29
|
- lib/yobi/io_handle.rb
|
|
@@ -62,7 +64,7 @@ metadata:
|
|
|
62
64
|
homepage_uri: https://codeberg.org/ukazap/yobi
|
|
63
65
|
source_code_uri: https://codeberg.org/ukazap/yobi
|
|
64
66
|
changelog_uri: https://codeberg.org/ukazap/yobi/src/branch/main/CHANGELOG.md
|
|
65
|
-
documentation_uri: https://rubydoc.info/gems/yobi/1.
|
|
67
|
+
documentation_uri: https://rubydoc.info/gems/yobi/1.1.0
|
|
66
68
|
rdoc_options: []
|
|
67
69
|
require_paths:
|
|
68
70
|
- lib
|