diff --git a/frontend/app.py b/frontend/app.py index 7975776..b610df8 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -194,6 +194,57 @@ def session_upload( "vmid": vmid, "vm_name": vm_name, "file_size_gb": file_size_gb, + }) + + else: + # ── Download ──────────────────────────────────────────────── + url = (source_url or "").strip() + if not url: + return JSONResponse({"phase": "error", "error": "No URL provided."}, status_code=400) + + filename = Path(url).name or f"download_{uuid.uuid4().hex[:8]}" + dest = STAGING / filename + + _active_downloads.pop(filename, None) + + usage = shutil.disk_usage(STAGING) + free_gb = usage.free / (1024**3) + if free_gb < 50: + logger.warning("Low disk: %.1f GB free — download may fail", free_gb) + + logger.info("Starting background download: %s → %s", url, dest) + try: + proc = subprocess.Popen( + ["wget", "--progress=dot:giga", "-O", str(dest), url], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + except Exception as exc: + return JSONResponse({"phase": "error", "error": f"Failed to start download: {exc}"}, status_code=500) + + _active_downloads[filename] = { + "proc": proc, "dest": dest, "vmid": vmid, "vm_name": vm_name, + "start_time": time.time(), "content_length": 0, "_last_logged_bytes": 0, + } + + content_length = 0 + def _fetch_cl(): + nonlocal content_length + try: + hr = http_requests.head(url, timeout=5, allow_redirects=True) + cl = hr.headers.get("Content-Length") + if cl: + content_length = int(cl) + if filename in _active_downloads: + _active_downloads[filename]["content_length"] = int(cl) + logger.info("Download size: %.1f GiB", int(cl) / (1024**3)) + except Exception: + pass + threading.Thread(target=_fetch_cl, daemon=True).start() + + return JSONResponse({ + "phase": "downloading", + "filename": filename, "vmid": vmid, "vm_name": vm_name, + "content_length_gb": round(content_length / (1024**3), 1) if content_length else None, }) @@ -247,63 +298,6 @@ async def upload_raw(request: Request): }) - # ── Download ──────────────────────────────────────────────────── - url = (source_url or "").strip() - if not url: - return JSONResponse({"phase": "error", "error": "No URL provided."}, status_code=400) - - filename = Path(url).name or f"download_{uuid.uuid4().hex[:8]}" - dest = STAGING / filename - - # Clean up any stale download with same name - _active_downloads.pop(filename, None) - - usage = shutil.disk_usage(STAGING) - free_gb = usage.free / (1024**3) - if free_gb < 50: - logger.warning("Low disk: %.1f GB free — download may fail", free_gb) - - # Start wget immediately (HEAD request for size happens in background) - logger.info("Starting background download: %s → %s", url, dest) - try: - proc = subprocess.Popen( - ["wget", "--progress=dot:giga", "-O", str(dest), url], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - ) - except Exception as exc: - return JSONResponse({"phase": "error", "error": f"Failed to start download: {exc}"}, status_code=500) - - _active_downloads[filename] = { - "proc": proc, - "dest": dest, - "vmid": vmid, - "vm_name": vm_name, - "start_time": time.time(), - "content_length": 0, - "_last_logged_bytes": 0, - } - - # Fire-and-forget HEAD request to get file size for ETA (non-blocking) - def _fetch_content_length(): - try: - head_resp = http_requests.head(url, timeout=5, allow_redirects=True) - cl = head_resp.headers.get("Content-Length") - if cl and filename in _active_downloads: - _active_downloads[filename]["content_length"] = int(cl) - logger.info("Download size from HEAD: %.1f GiB", int(cl) / (1024**3)) - except Exception: - pass - threading.Thread(target=_fetch_content_length, daemon=True).start() - - return JSONResponse({ - "phase": "downloading", - "filename": filename, - "vmid": vmid, - "vm_name": vm_name, - "content_length_gb": round(content_length / (1024**3), 1) if content_length else None, - }) - - @app.get("/session/progress/{filename}") async def session_progress(filename: str): """Poll download progress — returns current file size and phase."""