Files
geointel/scripts/release_backup_snapshot.py
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

377 lines
15 KiB
Python

#!/usr/bin/env python3
"""Create and verify byte-complete, symlink-safe release backup snapshots."""
from __future__ import annotations
import argparse
import errno
import hashlib
import json
import os
import re
import stat
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
MANIFEST_HEADER = "relative_path\tsize_bytes\tmtime_ns\tsha256"
SAFE_LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
FICLONE = 0x40049409
FICLONE_FALLBACK_ERRORS = {
errno.EXDEV,
errno.EOPNOTSUPP,
errno.ENOTTY,
errno.EINVAL,
errno.ENOSYS,
}
@dataclass(frozen=True)
class SourceEntry:
path: Path
relative_path: str
stat_result: os.stat_result
is_directory: bool
@dataclass(frozen=True)
class ManifestEntry:
relative_path: str
size_bytes: int
mtime_ns: int
sha256: str
def _safe_relative(value: str) -> str:
if not value or "\t" in value or "\n" in value or "\r" in value:
raise RuntimeError(f"Unsupported snapshot path: {value!r}")
candidate = PurePosixPath(value)
if candidate.is_absolute() or any(part in {"", ".", ".."} for part in candidate.parts):
raise RuntimeError(f"Unsafe snapshot path: {value!r}")
return candidate.as_posix()
def _collect(root: Path) -> list[SourceEntry]:
entries: list[SourceEntry] = []
for current, directory_names, file_names in os.walk(root, topdown=True, followlinks=False):
directory_names.sort()
file_names.sort()
current_path = Path(current)
for name, is_directory in [
*((name, True) for name in directory_names),
*((name, False) for name in file_names),
]:
path = current_path / name
details = path.lstat()
relative = _safe_relative(path.relative_to(root).as_posix())
if stat.S_ISLNK(details.st_mode):
raise RuntimeError(f"Release snapshot refuses symlinked content: {relative}")
if is_directory and not stat.S_ISDIR(details.st_mode):
raise RuntimeError(f"Snapshot directory changed during inventory: {relative}")
if not is_directory and not stat.S_ISREG(details.st_mode):
raise RuntimeError(f"Release snapshot refuses non-regular content: {relative}")
entries.append(SourceEntry(path, relative, details, is_directory))
return entries
def _same_file_state(before: os.stat_result, after: os.stat_result) -> bool:
return (
before.st_dev,
before.st_ino,
before.st_size,
before.st_mtime_ns,
) == (
after.st_dev,
after.st_ino,
after.st_size,
after.st_mtime_ns,
)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _copy_all(source_descriptor: int, destination_descriptor: int) -> None:
while True:
value = os.read(source_descriptor, 1024 * 1024)
if not value:
return
view = memoryview(value)
while view:
written = os.write(destination_descriptor, view)
if written <= 0:
raise RuntimeError("Snapshot copy stopped before writing all bytes")
view = view[written:]
def _clone_or_copy(entry: SourceEntry, destination: Path) -> ManifestEntry:
source_flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
destination_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
source_descriptor = os.open(entry.path, source_flags)
destination_descriptor = -1
try:
opened = os.fstat(source_descriptor)
if not stat.S_ISREG(opened.st_mode) or not _same_file_state(entry.stat_result, opened):
raise RuntimeError(f"Snapshot file changed before copying: {entry.relative_path}")
destination.parent.mkdir(parents=True, exist_ok=True)
destination_descriptor = os.open(destination, destination_flags, stat.S_IMODE(opened.st_mode))
cloned = False
if os.name == "posix":
try:
import fcntl
fcntl.ioctl(destination_descriptor, FICLONE, source_descriptor)
cloned = True
except OSError as exc:
if exc.errno not in FICLONE_FALLBACK_ERRORS:
raise
if not cloned:
os.lseek(source_descriptor, 0, os.SEEK_SET)
os.ftruncate(destination_descriptor, 0)
_copy_all(source_descriptor, destination_descriptor)
os.fsync(destination_descriptor)
after = os.fstat(source_descriptor)
if not _same_file_state(opened, after):
raise RuntimeError(f"Snapshot file changed while copying: {entry.relative_path}")
except BaseException:
if destination_descriptor >= 0:
os.close(destination_descriptor)
destination_descriptor = -1
destination.unlink(missing_ok=True)
raise
finally:
if destination_descriptor >= 0:
os.close(destination_descriptor)
os.close(source_descriptor)
os.chmod(destination, stat.S_IMODE(entry.stat_result.st_mode) & ~0o222, follow_symlinks=False)
retained_times = (entry.stat_result.st_atime_ns, entry.stat_result.st_mtime_ns)
try:
os.utime(destination, ns=retained_times, follow_symlinks=False)
except NotImplementedError:
# Windows does not expose no-follow utime. The destination was created
# exclusively above; recheck it before using the portable call.
if destination.is_symlink():
destination.unlink(missing_ok=True)
raise RuntimeError(f"Snapshot destination became a symlink: {entry.relative_path}")
os.utime(destination, ns=retained_times)
source_checksum = _sha256(entry.path)
snapshot_checksum = _sha256(destination)
final_source = entry.path.lstat()
if not _same_file_state(entry.stat_result, final_source):
raise RuntimeError(f"Snapshot file changed during checksum verification: {entry.relative_path}")
if source_checksum != snapshot_checksum:
raise RuntimeError(f"Snapshot checksum differs from source: {entry.relative_path}")
return ManifestEntry(
relative_path=entry.relative_path,
size_bytes=entry.stat_result.st_size,
mtime_ns=entry.stat_result.st_mtime_ns,
sha256=snapshot_checksum,
)
def _link_verified_prior(
entry: SourceEntry,
destination: Path,
prior_root: Path,
prior_manifest: dict[str, ManifestEntry],
) -> ManifestEntry | None:
retained = prior_manifest.get(entry.relative_path)
if retained is None or retained.size_bytes != entry.stat_result.st_size:
return None
source_checksum = _sha256(entry.path)
final_source = entry.path.lstat()
if not _same_file_state(entry.stat_result, final_source):
raise RuntimeError(f"Snapshot file changed during prior comparison: {entry.relative_path}")
if source_checksum != retained.sha256:
return None
prior_path = prior_root / entry.relative_path
try:
prior_details = prior_path.lstat()
except FileNotFoundError:
return None
if not stat.S_ISREG(prior_details.st_mode) or prior_details.st_size != retained.size_bytes:
raise RuntimeError(f"Prior snapshot file is not reusable: {entry.relative_path}")
if _sha256(prior_path) != retained.sha256:
raise RuntimeError(f"Prior snapshot checksum changed: {entry.relative_path}")
destination.parent.mkdir(parents=True, exist_ok=True)
os.link(prior_path, destination, follow_symlinks=False)
if destination.stat().st_size != retained.size_bytes or _sha256(destination) != retained.sha256:
destination.unlink(missing_ok=True)
raise RuntimeError(f"Hard-linked snapshot verification failed: {entry.relative_path}")
return ManifestEntry(
relative_path=entry.relative_path,
size_bytes=entry.stat_result.st_size,
mtime_ns=entry.stat_result.st_mtime_ns,
sha256=source_checksum,
)
def read_manifest(path: Path) -> dict[str, ManifestEntry]:
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0] != MANIFEST_HEADER:
raise RuntimeError(f"Snapshot inventory has an invalid header: {path}")
entries: dict[str, ManifestEntry] = {}
for line in lines[1:]:
fields = line.split("\t")
if len(fields) != 4:
raise RuntimeError(f"Snapshot inventory has an invalid row: {line!r}")
relative_path, size_text, mtime_text, checksum = fields
relative_path = _safe_relative(relative_path)
if relative_path in entries:
raise RuntimeError(f"Snapshot inventory repeats a path: {relative_path}")
try:
size_bytes = int(size_text)
mtime_ns = int(mtime_text)
except ValueError as exc:
raise RuntimeError(f"Snapshot inventory has invalid metadata: {relative_path}") from exc
if size_bytes < 0 or mtime_ns < 0 or not re.fullmatch(r"[0-9a-f]{64}", checksum):
raise RuntimeError(f"Snapshot inventory has invalid retained state: {relative_path}")
entries[relative_path] = ManifestEntry(relative_path, size_bytes, mtime_ns, checksum)
return entries
def verify_snapshot(snapshot_path: Path, manifest_path: Path) -> None:
root = snapshot_path.expanduser().resolve(strict=True)
if not root.is_dir():
raise RuntimeError(f"Snapshot path is not a directory: {root}")
expected = read_manifest(manifest_path)
observed_entries = _collect(root)
observed_files = {item.relative_path: item for item in observed_entries if not item.is_directory}
extra = sorted(set(observed_files) - set(expected))
missing = sorted(set(expected) - set(observed_files))
if extra:
raise RuntimeError(f"Snapshot contains unmanifested files: {', '.join(extra[:10])}")
if missing:
raise RuntimeError(f"Snapshot omits manifested files: {', '.join(missing[:10])}")
for relative, retained in expected.items():
current = observed_files[relative]
if current.stat_result.st_size != retained.size_bytes:
raise RuntimeError(f"Snapshot size differs for: {relative}")
if _sha256(current.path) != retained.sha256:
raise RuntimeError(f"Snapshot checksum differs for: {relative}")
def create_snapshot(
source: Path,
snapshot_path: Path,
manifest_path: Path,
*,
label: str,
link_dest_snapshot: Path | None = None,
link_dest_manifest: Path | None = None,
) -> None:
if not SAFE_LABEL.fullmatch(label):
raise RuntimeError(f"Unsafe snapshot label: {label!r}")
root = source.expanduser()
if root.is_symlink():
raise RuntimeError(f"Release snapshot refuses a symlinked root: {root}")
root = root.resolve(strict=True)
if not root.is_dir():
raise RuntimeError(f"Snapshot source is not a directory: {root}")
snapshot_path = snapshot_path.expanduser().resolve()
manifest_path = manifest_path.expanduser().resolve()
for output in (snapshot_path, manifest_path):
try:
output.relative_to(root)
except ValueError:
pass
else:
raise RuntimeError("Release snapshot output must not be inside its source tree")
if snapshot_path.exists():
raise RuntimeError(f"Snapshot destination already exists: {snapshot_path}")
snapshot_path.mkdir(parents=True, exist_ok=False)
manifest_path.parent.mkdir(parents=True, exist_ok=True)
prior_root: Path | None = None
prior_manifest: dict[str, ManifestEntry] = {}
if (link_dest_snapshot is None) != (link_dest_manifest is None):
raise RuntimeError("Prior snapshot and manifest must be supplied together")
if link_dest_snapshot is not None and link_dest_manifest is not None:
prior_root = link_dest_snapshot.expanduser().resolve(strict=True)
prior_manifest = read_manifest(link_dest_manifest.expanduser().resolve(strict=True))
initial = _collect(root)
retained: list[ManifestEntry] = []
for entry in initial:
destination = snapshot_path / entry.relative_path
if entry.is_directory:
destination.mkdir(parents=True, exist_ok=False)
os.chmod(destination, stat.S_IMODE(entry.stat_result.st_mode), follow_symlinks=False)
continue
linked = (
_link_verified_prior(entry, destination, prior_root, prior_manifest)
if prior_root is not None
else None
)
retained.append(linked or _clone_or_copy(entry, destination))
final = _collect(root)
if [(item.relative_path, item.is_directory) for item in initial] != [
(item.relative_path, item.is_directory) for item in final
]:
raise RuntimeError(f"Snapshot source contents changed while backup was running: {root}")
with manifest_path.open("w", encoding="utf-8", newline="\n") as handle:
handle.write(f"{MANIFEST_HEADER}\n")
for entry in retained:
handle.write(
f"{entry.relative_path}\t{entry.size_bytes}\t{entry.mtime_ns}\t{entry.sha256}\n"
)
verify_snapshot(snapshot_path, manifest_path)
def verify_backup(backup_dir: Path) -> None:
root = backup_dir.expanduser().resolve(strict=True)
payload = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
for label in ("storage", "models"):
requested = payload.get(f"{label}_inventory_requested") is True
snapshotted = payload.get(f"{label}_snapshot_requested") is True
if requested != snapshotted:
raise RuntimeError(f"Backup manifest does not bind the {label} inventory to a snapshot")
if requested:
verify_snapshot(root / f"{label}-snapshot", root / f"{label}-manifest.tsv")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
create = subparsers.add_parser("create")
create.add_argument("--source", type=Path, required=True)
create.add_argument("--snapshot", type=Path, required=True)
create.add_argument("--manifest", type=Path, required=True)
create.add_argument("--label", required=True)
create.add_argument("--link-dest-snapshot", type=Path)
create.add_argument("--link-dest-manifest", type=Path)
verify = subparsers.add_parser("verify")
verify.add_argument("--snapshot", type=Path, required=True)
verify.add_argument("--manifest", type=Path, required=True)
verify_backup_parser = subparsers.add_parser("verify-backup")
verify_backup_parser.add_argument("--backup-dir", type=Path, required=True)
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.command == "create":
create_snapshot(
args.source,
args.snapshot,
args.manifest,
label=args.label,
link_dest_snapshot=args.link_dest_snapshot,
link_dest_manifest=args.link_dest_manifest,
)
elif args.command == "verify":
verify_snapshot(args.snapshot, args.manifest)
else:
verify_backup(args.backup_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())