From a7ab9243c0668fd36c2328ebec513efac6d5538f Mon Sep 17 00:00:00 2001 From: Claus Lohmar Date: Fri, 24 Jul 2026 09:48:43 +0000 Subject: [PATCH] feat: SCP probes remote file size via SSH before pull Runs 'ssh stat -c%s' or 'ls -l' to get total bytes before starting SCP transfer. Progress bar now shows real percentage and ETA instead of indeterminate spinner. --- frontend/app.py | 34 +++++++++++++++++++++++++++++++++- frontend/templates/scp.html | 6 ++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/frontend/app.py b/frontend/app.py index b92200e..4ab0501 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -472,8 +472,25 @@ def scp_start( "-P", str(scp_port), remote, str(dest), ] - logger.info("Starting SCP pull: %s → %s", remote, dest) + + # ── Probe file size via SSH ──────────────────────────────────── + file_size = 0 + try: + probe_cmd = [ + "sshpass", "-e", + "ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", + "-p", str(scp_port), f"{scp_user}@{scp_host}", + f"stat -c%s '{scp_path}' 2>/dev/null || ls -l '{scp_path}' 2>/dev/null | awk '{{print $5}}'" + ] + probe = subprocess.run(probe_cmd, env=env, capture_output=True, text=True, timeout=15) + if probe.returncode == 0 and probe.stdout.strip().isdigit(): + file_size = int(probe.stdout.strip()) + logger.info("Remote file size: %.1f GiB", file_size / (1024**3)) + except Exception: + logger.info("Could not probe remote file size") + + # ── Start SCP ────────────────────────────────────────────────── try: proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) @@ -485,12 +502,14 @@ def scp_start( "proc": proc, "dest": dest, "start_time": time.time(), + "total_size": file_size, } return JSONResponse({ "phase": "pulling", "filename": filename, "session_id": session_id, + "total_size_gb": round(file_size / (1024**3), 1) if file_size else None, }) @@ -540,12 +559,25 @@ def scp_progress(session_id: str, filename: str): elapsed = max(time.time() - info.get("start_time", 0), 1) speed_mbps = round(current_bytes / elapsed / 1_000_000, 1) + total = info.get("total_size", 0) + + eta_str = None + pct = 0 + if total > 0 and current_bytes > 0: + pct = min(round((current_bytes / total) * 100), 99) + remaining = total - current_bytes + eta = remaining / max(current_bytes / elapsed, 1) + if eta > 60: + eta_str = f"~{round(eta / 60)} min remaining" return JSONResponse({ "phase": "pulling", "file_size_bytes": current_bytes, "file_size_gb": round(current_bytes / (1024**3), 1), "speed_mbps": speed_mbps, + "pct": pct, + "eta": eta_str, + "total_size_gb": round(total / (1024**3), 1) if total else None, }) diff --git a/frontend/templates/scp.html b/frontend/templates/scp.html index c20cf3c..741db34 100644 --- a/frontend/templates/scp.html +++ b/frontend/templates/scp.html @@ -95,7 +95,8 @@ async function startScpPull(e) { } const filename = data.filename; - setScpPhase('Pulling file from remote server...', 0, true); + const totalGb = data.total_size_gb; + setScpPhase('Pulling file...' + (totalGb ? ' (' + totalGb + ' GiB total)' : ''), 0, true); for (;;) { await new Promise(r => setTimeout(r, 2000)); try { @@ -123,7 +124,8 @@ async function startScpPull(e) { const gb = (pdata.file_size_gb || 0).toFixed(1); let label = 'Pulling file... ' + gb + ' GiB'; if (pdata.speed_mbps) label += ' (' + pdata.speed_mbps + ' MB/s)'; - setScpPhase(label, 0, true); + if (pdata.eta) label += ' — ' + pdata.eta; + setScpPhase(label, pdata.pct || 0, !pdata.pct); } } catch (_) {} }