From 3e7b777e53c268f59dadedb771b740e374888a16 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Tue, 22 Sep 2026 19:41:52 -0400 Subject: [PATCH] demo: read the recovered content instead of a path zerto_recover_file no longer writes the file to this host and hands back a path, because a path here means nothing to a caller elsewhere and letting the caller choose it was an arbitrary write. Both drivers still read rec["path"], so they broke. They now use the returned content. A small recovered_bytes() helper in each driver decodes the text or base64 form, so the Windows copy-back keeps shipping exact bytes rather than letting PowerShell rewrite line endings, which is the bug that put a stray CR in an earlier take. The Linux driver writes the bytes to a local file before scp, since scp needs something on disk to send. Verified against a live recovery: the helper returns 164 bytes whose sha256 matches the one the server reported, and the keys the on-screen show() filter uses are all still present. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn --- demo/driver.py | 21 +++++++++++++++++---- demo/windows_driver.py | 15 +++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/demo/driver.py b/demo/driver.py index 4df1bf2..60dde4f 100644 --- a/demo/driver.py +++ b/demo/driver.py @@ -16,6 +16,17 @@ CYAN, GREEN, RED, YEL, MAG = "\033[36m", "\033[32m", "\033[31m", "\033[33m", "\0 SPEED = float(os.environ.get("DEMO_SPEED", "1.0")) + +def recovered_bytes(rec: dict) -> bytes: + """zerto_recover_file returns the content now, not a path on the MCP host. + + The server may be on another machine, so a path there is of no use here. + """ + import base64 + if rec.get("encoding") == "base64": + return base64.b64decode(rec["content"]) + return rec["content"].encode() + def w(s=""): sys.stdout.write(s + "\n"); sys.stdout.flush() @@ -212,8 +223,8 @@ async def main(): if not rec.get("ok"): w(f"{RED} FLR failed{RST}"); return 1 beat(1.5) - w(f"{YEL} $ cat {rec['path']}{RST}") - body = Path(rec["path"]).read_text() + w(f"{YEL} recovered {rec['bytes']} bytes, sha256 {rec['sha256'][:16]}...{RST}") + body = recovered_bytes(rec).decode() for line in body.splitlines(): w(f"{GREEN}{BOLD} | {line}{RST}") beat(2.5) @@ -222,11 +233,13 @@ async def main(): step("Copy it back to the guest") note("This part is scp, not Zerto. Zerto got the bytes back.") target = f"{CFG['user']}@{CFG['host']}" + local_copy = SP / rec["name"] + local_copy.write_bytes(recovered_bytes(rec)) scp = (["sshpass", "-p", CFG["password"]] if CFG.get("password") else []) + \ ["scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR"] + (["-i", CFG["key"]] if CFG.get("key") else []) + \ - [rec["path"], f"{target}:{CFG['guest_path']}"] - w(f"{YEL} $ scp {Path(rec['path']).name} {target}:{CFG['guest_path']}{RST}") + [str(local_copy), f"{target}:{CFG['guest_path']}"] + w(f"{YEL} $ scp {rec['name']} {target}:{CFG['guest_path']}{RST}") subprocess.run(scp, capture_output=True, text=True, timeout=60) guest(f"cat {CFG['guest_path']}") beat(1.5) diff --git a/demo/windows_driver.py b/demo/windows_driver.py index 2233b69..b89ae24 100644 --- a/demo/windows_driver.py +++ b/demo/windows_driver.py @@ -22,6 +22,17 @@ T0 = time.time() STEP = [0] + +def recovered_bytes(rec: dict) -> bytes: + """zerto_recover_file returns the content now, not a path on the MCP host. + + The server may be on another machine, so a path there is of no use here. + """ + import base64 + if rec.get("encoding") == "base64": + return base64.b64decode(rec["content"]) + return rec["content"].encode() + def w(s=""): sys.stdout.write(s + "\n"); sys.stdout.flush() @@ -155,7 +166,7 @@ async def main(): show(rec, keep=["ok", "bytes", "flr_path", "unmount"]) if not rec.get("ok"): w(f"{RED} FLR failed{RST}"); return 1 - body = Path(rec["path"]).read_text() + body = recovered_bytes(rec).decode() w(f"{YEL} recovered file:{RST}") for line in body.splitlines(): w(f"{GREEN}{BOLD} | {line}{RST}") @@ -166,7 +177,7 @@ async def main(): # endings and appends one, which makes the restored file differ # from what Zerto handed back. WriteAllBytes does not. import base64 - blob = Path(rec["path"]).read_bytes() + blob = recovered_bytes(rec) b64 = base64.b64encode(blob).decode() guest_ps( f"[IO.File]::WriteAllBytes('{CFG['guest_path']}',"