diff --git a/README.md b/README.md index 9e1b761..70a68eb 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,14 @@ restore, use another dry run before writing to the remote root: rsync -aHAX --numeric-ids --info=progress2 --dry-run /backups/example.org/scheduled//data/ root@example.org:/ ``` +For most incidents, prefer a targeted restore instead of copying the whole snapshot. Keep paths relative to the +snapshot's `data/` directory: + +``` +rsync -aHAX --numeric-ids --info=progress2 --dry-run /backups/example.org/scheduled//data/etc/nginx/ /restore/example.org/etc/nginx/ +rsync -aHAX --numeric-ids --info=progress2 --dry-run /backups/example.org/scheduled//data/home/example/site/public_html/index.php /restore/example.org/home/example/site/public_html/index.php +``` + Snapshots may use hardlinks for files that are unchanged between backups. That saves disk space and is safe for normal restore copies, but do not edit files inside snapshot directories. Treat snapshots as read-only and copy data out with rsync. diff --git a/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html b/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html index 2eba221..f09d0f8 100644 --- a/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/snapshot_detail.html @@ -82,6 +82,16 @@
Restore to staging:
{{ restore.local_command }}
+
+
Dry-run a directory restore:
+
{{ restore.partial_dry_run_command }}
+
Replace {{ restore.example_relative_path }} with the path you want to restore.
+
+
+
Dry-run a single file restore:
+
{{ restore.file_dry_run_command }}
+
Replace {{ restore.example_file_relative_path }} with the file you want to restore.
+
Dry-run restore back to the source host:
{{ restore.remote_dry_run_command }}
diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index 9a06d6b..031d0ed 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -1427,6 +1427,12 @@ class ViewTests(TestCase): self.assertContains(response, "rsync -aHAX --numeric-ids --info=progress2 --dry-run") self.assertContains(response, f"{base.path}/data/") self.assertContains(response, "root@web-01.example.test:/") + self.assertContains(response, "Dry-run a directory restore") + self.assertContains(response, f"{base.path}/data/etc/nginx/") + self.assertContains(response, f"/restore/{host.host}/etc/nginx/") + self.assertContains(response, "Dry-run a single file restore") + self.assertContains(response, f"{base.path}/data/home/example/site/public_html/index.php") + self.assertContains(response, f"/restore/{host.host}/home/example/site/public_html/index.php") self.assertContains(response, "Treat snapshot directories as read-only") self.assertContains(response, child.dirname) self.assertContains(response, f"Run {run.id}") diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index 725af7b..bc78428 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -796,17 +796,27 @@ def _pretty_json(value: object) -> str: def _snapshot_restore_guidance(snapshot: SnapshotRecord) -> dict[str, str]: source_path = Path(snapshot.path) / "data" destination_path = Path("/restore") / snapshot.host.host + example_relative_path = Path("etc") / "nginx" + example_file_relative_path = Path("home") / "example" / "site" / "public_html" / "index.php" quoted_source = _quote_path_with_trailing_slash(source_path) quoted_destination = _quote_path_with_trailing_slash(destination_path) + quoted_partial_source = _quote_path_with_trailing_slash(source_path / example_relative_path) + quoted_partial_destination = _quote_path_with_trailing_slash(destination_path / example_relative_path) + quoted_file_source = shlex.quote(str(source_path / example_file_relative_path)) + quoted_file_destination = shlex.quote(str(destination_path / example_file_relative_path)) quoted_remote_destination = shlex.quote(f"root@{snapshot.host.address or snapshot.host.host}:/") common_args = "rsync -aHAX --numeric-ids --info=progress2" return { "source_path": str(source_path), "destination_path": str(destination_path), + "example_relative_path": str(example_relative_path), + "example_file_relative_path": str(example_file_relative_path), "inspect_command": f"ls -la {quoted_source}", "dry_run_command": f"{common_args} --dry-run {quoted_source} {quoted_destination}", "local_command": f"{common_args} {quoted_source} {quoted_destination}", + "partial_dry_run_command": f"{common_args} --dry-run {quoted_partial_source} {quoted_partial_destination}", + "file_dry_run_command": f"{common_args} --dry-run {quoted_file_source} {quoted_file_destination}", "remote_dry_run_command": f"{common_args} --dry-run {quoted_source} {quoted_remote_destination}", }