From 8eb1def62b3840b38a3e8d0eb82c77d8f7cbe595 Mon Sep 17 00:00:00 2001 From: Claus Lohmar Date: Mon, 27 Jul 2026 07:38:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20session=20resume=20=E2=80=94=20detect?= =?UTF-8?q?=20leftover=20staging=20dirs=20and=20offer=20to=20resume=20anal?= =?UTF-8?q?ysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/v1/sessions scans tmp/ for session dirs with staged files - Index page shows resumable sessions with filenames and sizes - Resume button skips download and starts analysis with stored session_id - session_id carried through analysis → confirm → job for correct file paths --- backend/app.py | 38 ++++++++++++++++++++ frontend/api_client.py | 4 +++ frontend/app.py | 13 ++++++- frontend/templates/_analysis.html | 1 + frontend/templates/index.html | 58 ++++++++++++++++++++++++++++--- 5 files changed, 109 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index f68f0a9..a70ac4a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -139,6 +139,44 @@ def storage_pools(): return {"pools": result} +@app.get(f"{API_PREFIX}/sessions") +def list_sessions(): + """Return existing session directories with staged files that can be resumed.""" + tmp = Path("/mnt/converter/tmp") if Path("/mnt/converter/tmp").exists() else Path(__file__).resolve().parent.parent / "tmp" + sessions = [] + try: + for entry in sorted(tmp.iterdir(), reverse=True): + if not entry.is_dir(): + continue + in_dir = entry / "in" + if not in_dir.is_dir(): + continue + session_files = [] + total_bytes = 0 + has_disks = False + for f in sorted(in_dir.iterdir()): + if f.is_file(): + sz = f.stat().st_size + session_files.append({"name": f.name, "size_bytes": sz, "size_gb": round(sz / (1024**3), 1)}) + total_bytes += sz + elif f.is_dir(): + for df in f.rglob("*"): + if df.is_file() and df.suffix.lower() in {".vmdk", ".qcow2", ".img", ".raw", ".vhd", ".vhdx"}: + has_disks = True + break + if not session_files: + continue + sessions.append({ + "session_id": entry.name, + "files": session_files, + "total_size_gb": round(total_bytes / (1024**3), 1), + "has_disks": has_disks, + }) + except Exception: + pass + return {"sessions": sessions} + + @app.post(f"{API_PREFIX}/analyze", response_model=AnalyzeStatusResponse, status_code=202) def analyze(req: AnalyzeRequest) -> AnalyzeStatusResponse: """Analyze a source file asynchronously: extraction + disk probing + OS detection.""" diff --git a/frontend/api_client.py b/frontend/api_client.py index 43c8aef..afe3507 100644 --- a/frontend/api_client.py +++ b/frontend/api_client.py @@ -58,6 +58,10 @@ class ApiClient: """GET /api/v1/storage/pools — list available Proxmox storage pools.""" return self._get("/api/v1/storage/pools") + def get_sessions(self) -> dict: + """GET /api/v1/sessions — list resumable staging sessions.""" + return self._get("/api/v1/sessions") + def create_job(self, payload: dict) -> dict: """POST /api/v1/jobs — submit conversion job.""" return self._post("/api/v1/jobs", payload) diff --git a/frontend/app.py b/frontend/app.py index 87b1b46..30ae6af 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -337,6 +337,15 @@ async def session_analyze_status(analysis_id: str): return JSONResponse({"error": exc.detail}, status_code=502) +@app.get("/session/sessions") +async def session_list(): + """Proxy to backend to list resumable sessions.""" + try: + return api.get_sessions() + except ApiError as exc: + return JSONResponse({"sessions": []}) + + @app.get("/session/analyze/result/{analysis_id}", response_class=HTMLResponse) async def session_analyze_result( request: Request, @@ -344,6 +353,7 @@ async def session_analyze_result( vmid: str = "", source_filename: str = "", vm_name: str = "", + session_id: str = "", ): """Render the analysis result after polling completes.""" try: @@ -370,7 +380,8 @@ async def session_analyze_result( return render("_analysis.html", request=request, vmid=vmid, source_filename=source_filename, - vm_name=name, analysis=analysis, storage_pools=pools) + vm_name=name, analysis=analysis, storage_pools=pools, + session_id=session_id) @app.post("/session/confirm", response_class=HTMLResponse) diff --git a/frontend/templates/_analysis.html b/frontend/templates/_analysis.html index df80fc6..f4cc31e 100644 --- a/frontend/templates/_analysis.html +++ b/frontend/templates/_analysis.html @@ -31,6 +31,7 @@ +
diff --git a/frontend/templates/index.html b/frontend/templates/index.html index e5c194b..3e1369b 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -1,6 +1,14 @@ {% extends "base.html" %} {% block content %} +
+
+ Resume Previous Session +
+
+
+
+
New Conversion Session @@ -53,6 +61,46 @@