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) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
This commit is contained in:
justinandClaude Opus 5 committed 2026-09-22 19:41:52 -04:00
1 parent e802d86196
commit 3e7b777e53
2 files changed
+30 -6

No files matched your search

+17 -4
View File
@@ -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")) 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=""): def w(s=""):
sys.stdout.write(s + "\n"); sys.stdout.flush() sys.stdout.write(s + "\n"); sys.stdout.flush()
@@ -212,8 +223,8 @@ async def main():
if not rec.get("ok"): if not rec.get("ok"):
w(f"{RED} FLR failed{RST}"); return 1 w(f"{RED} FLR failed{RST}"); return 1
beat(1.5) beat(1.5)
w(f"{YEL} $ cat {rec['path']}{RST}") w(f"{YEL} recovered {rec['bytes']} bytes, sha256 {rec['sha256'][:16]}...{RST}")
body = Path(rec["path"]).read_text() body = recovered_bytes(rec).decode()
for line in body.splitlines(): for line in body.splitlines():
w(f"{GREEN}{BOLD} | {line}{RST}") w(f"{GREEN}{BOLD} | {line}{RST}")
beat(2.5) beat(2.5)
@@ -222,11 +233,13 @@ async def main():
step("Copy it back to the guest") step("Copy it back to the guest")
note("This part is scp, not Zerto. Zerto got the bytes back.") note("This part is scp, not Zerto. Zerto got the bytes back.")
target = f"{CFG['user']}@{CFG['host']}" 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 = (["sshpass", "-p", CFG["password"]] if CFG.get("password") else []) + \
["scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", ["scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR"] + (["-i", CFG["key"]] if CFG.get("key") else []) + \ "-o", "LogLevel=ERROR"] + (["-i", CFG["key"]] if CFG.get("key") else []) + \
[rec["path"], f"{target}:{CFG['guest_path']}"] [str(local_copy), f"{target}:{CFG['guest_path']}"]
w(f"{YEL} $ scp {Path(rec['path']).name} {target}:{CFG['guest_path']}{RST}") w(f"{YEL} $ scp {rec['name']} {target}:{CFG['guest_path']}{RST}")
subprocess.run(scp, capture_output=True, text=True, timeout=60) subprocess.run(scp, capture_output=True, text=True, timeout=60)
guest(f"cat {CFG['guest_path']}") guest(f"cat {CFG['guest_path']}")
beat(1.5) beat(1.5)
+13 -2
View File
@@ -22,6 +22,17 @@ T0 = time.time()
STEP = [0] 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=""): def w(s=""):
sys.stdout.write(s + "\n"); sys.stdout.flush() sys.stdout.write(s + "\n"); sys.stdout.flush()
@@ -155,7 +166,7 @@ async def main():
show(rec, keep=["ok", "bytes", "flr_path", "unmount"]) show(rec, keep=["ok", "bytes", "flr_path", "unmount"])
if not rec.get("ok"): if not rec.get("ok"):
w(f"{RED} FLR failed{RST}"); return 1 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}") w(f"{YEL} recovered file:{RST}")
for line in body.splitlines(): for line in body.splitlines():
w(f"{GREEN}{BOLD} | {line}{RST}") w(f"{GREEN}{BOLD} | {line}{RST}")
@@ -166,7 +177,7 @@ async def main():
# endings and appends one, which makes the restored file differ # endings and appends one, which makes the restored file differ
# from what Zerto handed back. WriteAllBytes does not. # from what Zerto handed back. WriteAllBytes does not.
import base64 import base64
blob = Path(rec["path"]).read_bytes() blob = recovered_bytes(rec)
b64 = base64.b64encode(blob).decode() b64 = base64.b64encode(blob).decode()
guest_ps( guest_ps(
f"[IO.File]::WriteAllBytes('{CFG['guest_path']}'," f"[IO.File]::WriteAllBytes('{CFG['guest_path']}',"