Matthias Nott
2026-02-21 7300351bb9fb147f4de81a60423c4561a4924c21
app/app/routers/restore.py
....@@ -6,7 +6,7 @@
66 from fastapi.responses import StreamingResponse
77
88 from app.auth import verify_token
9
-from app.ops_runner import _BACKUP_TIMEOUT, stream_ops
9
+from app.ops_runner import _BACKUP_TIMEOUT, stream_ops_host
1010
1111 router = APIRouter()
1212
....@@ -22,18 +22,22 @@
2222 source: str,
2323 dry_run: bool,
2424 ) -> AsyncGenerator[str, None]:
25
- """Async generator that drives the restore workflow and yields SSE events."""
25
+ """Async generator that drives the restore workflow and yields SSE events.
26
+
27
+ Runs on the host via nsenter because ops restore delegates to project CLIs
28
+ that use host Python venvs incompatible with the container's Python.
29
+ """
2630 base_args = ["restore", project, env]
2731 if dry_run:
2832 base_args.append("--dry-run")
2933
3034 if source == "offsite":
31
- # ops offsite restore <project> <env> — downloads from offsite storage
35
+ # ops offsite restore <project> <env>
3236 download_args = ["offsite", "restore", project, env]
3337 yield _sse_line({"line": f"Downloading {project}/{env} from offsite...", "timestamp": _now()})
3438
3539 download_ok = True
36
- async for line in stream_ops(download_args, timeout=_BACKUP_TIMEOUT):
40
+ async for line in stream_ops_host(download_args, timeout=_BACKUP_TIMEOUT):
3741 yield _sse_line({"line": line, "timestamp": _now()})
3842 if line.startswith("[error]"):
3943 download_ok = False
....@@ -45,7 +49,7 @@
4549 yield _sse_line({"line": "Download complete. Starting restore...", "timestamp": _now()})
4650
4751 success = True
48
- async for line in stream_ops(base_args, timeout=_BACKUP_TIMEOUT):
52
+ async for line in stream_ops_host(base_args, timeout=_BACKUP_TIMEOUT):
4953 yield _sse_line({"line": line, "timestamp": _now()})
5054 if line.startswith("[error]"):
5155 success = False
....@@ -69,7 +73,7 @@
6973 Restore a backup for the given project/env.
7074
7175 Uses Server-Sent Events (SSE) to stream real-time progress.
72
- Parameters are passed as query strings since EventSource only supports GET.
76
+ Runs on the host via nsenter for Python venv compatibility.
7377 """
7478 return StreamingResponse(
7579 _restore_generator(project, env, source, dry_run),