> ## Content Index
> Fetch the complete content index at: https://agarhouse.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Test Your Backup Before You Need It: A Python Restore Drill
- URL: https://agarhouse.com/test-your-backup-python-restore-drill/
- Published: 2026-11-04T13:00:00.000Z
- Updated: 2026-11-04T13:00:00.000Z
- Description: Practice recovering a folder in a disposable Python lab. Create an archive, simulate sample-file damage, and verify the restored copy without overwriting the source.
- Author: Meatball
- Tags: Software Development, Tutorial, Python, #agarhouse-slot-2026-11-04-0800

A backup becomes much more useful when you know how to recover it. Creating an archive is only the first step: you also need to extract it somewhere safe and check that the recovered files contain what you expected.

This tutorial walks through a small restore drill using Python’s standard library. You will create a sample project, inventory it, archive the folder, inspect the archive, and restore it into a new empty directory. The final checks compare file contents and paths and confirm that restoration did not overwrite the source.

## Before you start

- Use a Linux terminal and a current, patched Python 3.12 or newer with `tarfile.data_filter` available. This exact example was tested on Linux with Python 3.12.14.
- Use a writable working directory and enough temporary disk space for a tiny sample project, its archive, and a restored copy. No additional packages or administrator privileges are needed.
- This example includes a relative symbolic link. It is not a verified Windows walkthrough.

**Important:** this script performs real filesystem writes. It deliberately changes one sample file and deletes another to demonstrate recovery. Run it unchanged: do not replace its source with your documents or another existing project. Every invocation creates a separate temporary lab containing only disposable sample data.

The archive stays on the same disk for this exercise. That is useful for learning a restore workflow, but it does not protect against disk failure, ransomware, or account compromise. A real backup plan needs independent storage and retention as well as repeat restore checks.

## 1\. Check Python and save the script

Check which Python your terminal will run:

```
python3 --version
python3 -c 'import tarfile; print("Extraction filter available:", hasattr(tarfile, "data_filter"))'
```

Confirm the version is 3.12 or newer and the second command prints `Extraction filter available: True`. If either requirement is missing, stop and update Python through your usual trusted installation method.

Save the complete code below as `restore_lab.py` in your working directory. Do not save over a file you already need.

```
#!/usr/bin/env python3
"""Controlled local recovery drill. Never point this script at real files."""
from pathlib import Path
import hashlib
import json
import os
import platform
import tarfile
import tempfile

if not hasattr(tarfile, "data_filter"):
    raise SystemExit("Stop: this Python lacks tarfile.data_filter; update Python.")

def inventory(root):
    result = {}
    for path in sorted(root.rglob("*")):
        name = path.relative_to(root).as_posix()
        if path.is_symlink():
            result[name] = ["symlink", os.readlink(path)]
        elif path.is_dir():
            result[name] = ["directory"]
        elif path.is_file():
            with path.open("rb") as stream:
                digest = hashlib.file_digest(stream, "sha256").hexdigest()
            result[name] = ["file", digest]
        else:
            raise RuntimeError(f"Unsupported file type: {path}")
    return result

def verify(actual, expected, label):
    if actual != expected:
        raise RuntimeError(f"FAIL: {label}")
    print(f"PASS: {label}")

lab = Path(tempfile.mkdtemp(prefix="backup-drill-"))
source = lab / "project"
(source / "notes").mkdir(parents=True)
(source / "empty").mkdir()
(source / "notes" / "plan.txt").write_text("Original plan\n", encoding="utf-8")
(source / ".settings").write_text("theme=dark\n", encoding="utf-8")
(source / "latest").symlink_to("notes/plan.txt")
print("Python:", platform.python_version())
print("Lab:", lab)

# Read-only inventory, not a tarfile dry run. Fixture creation above wrote files.
expected = inventory(source)
print("Preview:", ", ".join(expected))
with (lab / "manifest.json").open("x", encoding="utf-8") as stream:
    json.dump(expected, stream, indent=2)

# ACTUAL WRITE: create one new archive outside the source directory.
archive = lab / "project.tar.gz"
with tarfile.open(archive, "x:gz", dereference=False) as bundle:
    bundle.add(source, arcname="project")
print("Created:", archive.name)
with tarfile.open(archive, "r:gz") as bundle:
    print("Archive members:", ", ".join(bundle.getnames()))

# Deliberate damage ONLY to files this script just created in the isolated lab.
(source / "notes" / "plan.txt").write_text("Changed after backup\n", encoding="utf-8")
(source / ".settings").unlink()
if inventory(source) == expected:
    raise RuntimeError("Damage simulation did not change the sample")
print("Confirmed: sample source changed and .settings was deleted")

# ACTUAL WRITE: restore to a newly created staging directory, never over source.
staging = lab / "restore-check"
staging.mkdir()  # Fails if a path already exists.
with tarfile.open(archive, "r:gz") as bundle:
    bundle.extractall(staging, filter="data")
with (lab / "manifest.json").open(encoding="utf-8") as stream:
    expected = json.load(stream)
verify(inventory(staging / "project"), expected,
       "restored paths, file bytes, directories, and symlink targets match")
verify((source / "notes" / "plan.txt").read_text(encoding="utf-8"),
       "Changed after backup\n", "restore did not overwrite the changed source")
verify((source / ".settings").exists(), False,
       "restore did not recreate the deleted file in the source")
verify((staging / "project" / "latest").read_text(encoding="utf-8"),
       "Original plan\n", "restored relative symlink reaches restored file")
print("Keep for inspection:", lab)

```

## 2\. Run the complete restore drill

From the directory where you saved the script, run:

```
python3 restore_lab.py
```

The first output lines show the Python version and a newly allocated lab directory, normally under `/tmp`. Keep the exact path printed on your own machine. A later run will have a different directory name.

The script first creates the sample folder `project` with a text file, a hidden settings file, an empty directory, and a relative symlink. Once that folder exists, it records a manifest before making the archive. For regular files, the manifest stores SHA-256 content hashes; for directories and links, it records the entry type and, where applicable, the link target.

Next, `x:gz` creates a new compressed tar archive exclusively. If the archive destination already exists, creation fails rather than replacing it. The archive lives outside the source folder, so it is not added to itself. Setting `arcname="project"` gives its contents a predictable top-level directory.

Before any simulated damage, the script reopens the archive to list its members. On the tested run, that portion printed:

```
Preview: .settings, empty, latest, notes, notes/plan.txt
Created: project.tar.gz
Archive members: project, project/.settings, project/empty, project/latest, project/notes, project/notes/plan.txt
```

The code then changes the sample text and deletes the sample settings file. Those are intentional changes inside the newly created lab, not instructions to delete anything from a real project.

## 3\. Check the restored copy

Restoration creates `restore-check` using `mkdir()` without `exist_ok=True`. An existing destination makes the operation stop. The archive is extracted there, leaving the changed source folder in place.

The extraction explicitly uses `filter="data"`. This rejects several dangerous archive entries, including paths or links that escape the destination. It is not a complete sandbox or a guarantee against malicious archives and resource exhaustion. For this tutorial, extract only the archive the script just created. See the [Python tarfile documentation](https://docs.python.org/3.12/library/tarfile.html?ref=agarhouse.com) for filter behavior and additional precautions.

In the tested run, all four verification checks passed:

```
PASS: restored paths, file bytes, directories, and symlink targets match
PASS: restore did not overwrite the changed source
PASS: restore did not recreate the deleted file in the source
PASS: restored relative symlink reaches restored file
```

The first check compares the restored inventory with the saved manifest. The next two show that restoring elsewhere left the changed source unchanged. The last check reads through the restored relative symlink and confirms that it reaches the restored text file.

The lab remains available for inspection. Open the exact directory shown after `Keep for inspection:` in your file manager. It contains the changed `project` folder, `manifest.json`, `project.tar.gz`, and the restored `restore-check/project` folder. In the restored copy, `notes/plan.txt` contains `Original plan`, and `.settings` is present again. Enable hidden files in your file manager to see it.

## What a passing result does and does not prove

This drill verifies relative paths, entry types, regular-file contents, empty directories, and symlink target text for this sample. The SHA-256 comparison uses Python’s [hashlib module](https://docs.python.org/3.12/library/hashlib.html?ref=agarhouse.com). It detects content differences against the saved manifest; it does not establish authenticity if someone can replace both the archive and the manifest.

The archive preserves this relative symlink rather than following it. Absolute links and links to files outside the project are outside this exercise’s scope. Do not generalize the successful link check to every possible filesystem layout.

The comparison does not verify permissions, ownership, timestamps, ACLs, extended attributes, sparse-file behavior, or hard-link identity. The extraction filter can also adjust metadata. This is a file-tree recovery rehearsal, not a full-system restore or a promise of exact metadata preservation.

It also does not produce a consistent snapshot of files that other programs are changing. For real projects, stop writers or use an appropriate snapshot mechanism. Live databases need database-aware backup and recovery procedures rather than this demonstration.

## If something goes wrong

- **Python or filter check fails:** use a maintained Python meeting the prerequisites. Do not remove the guard or the extraction filter to make the script run.
- **FileExistsError:** an archive or restore destination already exists. Do not change exclusive creation to overwrite mode. Inspect the path in the error. An unchanged rerun allocates a fresh lab instead of reusing the previous one.
- **Permission, disk-space, or symlink error:** check that your temporary filesystem is writable, has space, and supports symbolic links. A failed run can leave a partial lab.
- **Extraction error:** stop and keep the archive and lab for inspection. Failed extraction can leave partial output; do not treat it as a successful restore or retry into that same destination.
- **A FAIL message or missing PASS lines:** the drill has not completed successfully. Read the traceback and inspect the printed lab path rather than moving recovered files over the source.

## Clean up only the disposable lab

There is no automatic cleanup command. When you have finished inspecting the results, use your file manager to remove only the exact `backup-drill-…` directory printed by this run. Verify its full path and its sample contents first. Do not delete your temporary directory itself, use a wildcard, or remove a folder containing your own files. Failed runs and repeated runs may each leave a separate lab.

For your next real backup, schedule a restore rehearsal into a fresh location and decide what a successful recovery must include. Check the recovered files before relying on the backup, and keep the backup on an independent storage destination. Do not repoint this deliberately destructive teaching script at your real data.

## Sources

- [Python 3.12: tarfile, archive modes, links, and extraction filters](https://docs.python.org/3.12/library/tarfile.html?ref=agarhouse.com)
- [Python 3.12: hashlib and file digests](https://docs.python.org/3.12/library/hashlib.html?ref=agarhouse.com)
- [Python 3.12: tempfile and temporary directory creation](https://docs.python.org/3.12/library/tempfile.html?ref=agarhouse.com)