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.
This commit is contained in:
parent
cd5f31505a
commit
a7ab9243c0
2 changed files with 37 additions and 3 deletions
|
|
@ -472,8 +472,25 @@ def scp_start(
|
||||||
"-P", str(scp_port),
|
"-P", str(scp_port),
|
||||||
remote, str(dest),
|
remote, str(dest),
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info("Starting SCP pull: %s → %s", remote, 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:
|
try:
|
||||||
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL,
|
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE, text=True)
|
stderr=subprocess.PIPE, text=True)
|
||||||
|
|
@ -485,12 +502,14 @@ def scp_start(
|
||||||
"proc": proc,
|
"proc": proc,
|
||||||
"dest": dest,
|
"dest": dest,
|
||||||
"start_time": time.time(),
|
"start_time": time.time(),
|
||||||
|
"total_size": file_size,
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"phase": "pulling",
|
"phase": "pulling",
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"session_id": session_id,
|
"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)
|
elapsed = max(time.time() - info.get("start_time", 0), 1)
|
||||||
speed_mbps = round(current_bytes / elapsed / 1_000_000, 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({
|
return JSONResponse({
|
||||||
"phase": "pulling",
|
"phase": "pulling",
|
||||||
"file_size_bytes": current_bytes,
|
"file_size_bytes": current_bytes,
|
||||||
"file_size_gb": round(current_bytes / (1024**3), 1),
|
"file_size_gb": round(current_bytes / (1024**3), 1),
|
||||||
"speed_mbps": speed_mbps,
|
"speed_mbps": speed_mbps,
|
||||||
|
"pct": pct,
|
||||||
|
"eta": eta_str,
|
||||||
|
"total_size_gb": round(total / (1024**3), 1) if total else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@ async function startScpPull(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = data.filename;
|
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 (;;) {
|
for (;;) {
|
||||||
await new Promise(r => setTimeout(r, 2000));
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
try {
|
try {
|
||||||
|
|
@ -123,7 +124,8 @@ async function startScpPull(e) {
|
||||||
const gb = (pdata.file_size_gb || 0).toFixed(1);
|
const gb = (pdata.file_size_gb || 0).toFixed(1);
|
||||||
let label = 'Pulling file... ' + gb + ' GiB';
|
let label = 'Pulling file... ' + gb + ' GiB';
|
||||||
if (pdata.speed_mbps) label += ' (' + pdata.speed_mbps + ' MB/s)';
|
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 (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue