diff --git a/backend/models.py b/backend/models.py index 744c614..f1bb243 100644 --- a/backend/models.py +++ b/backend/models.py @@ -57,6 +57,7 @@ class JobSubmissionRequest(BaseModel): target_storage: str = "local-lvm" additional_disks: List[DiskSpec] = [] target_disk_size_gb: Optional[int] = None + session_id: str = "" class CleanupRequest(BaseModel): diff --git a/backend/provisioner.py b/backend/provisioner.py index d983329..ec81bd1 100644 --- a/backend/provisioner.py +++ b/backend/provisioner.py @@ -32,6 +32,9 @@ logger = logging.getLogger("backend.provisioner") STAGING_OUT = Path("/mnt/converter/out") +def _staging_out(session_id: str = "") -> Path: + return STAGING_OUT / session_id if session_id else STAGING_OUT + # In-memory job tracking — survives as long as the process runs _jobs: dict[str, dict] = {} _jobs_lock = threading.Lock() @@ -117,9 +120,9 @@ def clone_vm(req: CloneRequest) -> JobStatusResponse: ) -def cleanup_staging(vmid: int, delete: bool) -> dict: +def cleanup_staging(vmid: int, delete: bool, session_id: str = "") -> dict: """Remove staging files for a VM ID.""" - out_dir = STAGING_OUT / str(vmid) + out_dir = _staging_out(session_id) / str(vmid) if not delete: return { @@ -220,7 +223,7 @@ def _process_job(job_id: str, req: JobSubmissionRequest): _update_job(job_id, status=JobStatus.PROCESSING_CONVERSION, progress=10, message="Converting disk image...") - out_dir = STAGING_OUT / str(vmid) + out_dir = _staging_out(req.session_id) / str(vmid) out_dir.mkdir(parents=True, exist_ok=True) # Collect all disks (boot disk + additional) @@ -232,7 +235,8 @@ def _process_job(job_id: str, req: JobSubmissionRequest): if spec.disk_type == DiskType.IMAGE_FILE: # Extract archive if needed, then locate the actual disk image - source = extract_if_needed(spec.source_filename) + src_path = _staging_in(req.session_id) / spec.source_filename + source = extract_if_needed(str(src_path)) disk = discover_disk(source) source_path = disk.path logger.info("Disk %d: source=%s format=%s size=%.1f GiB", diff --git a/frontend/app.py b/frontend/app.py index 9746886..b3a1696 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -63,6 +63,17 @@ app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static") _jinja = Environment(loader=FileSystemLoader(str(BASE / "templates")), autoescape=True) STAGING = Path("/mnt/converter/in") +def _staging(session_id: str = "") -> Path: + """Session-aware staging directory.""" + return STAGING / session_id if session_id else STAGING + +def _get_session_id(request: Request) -> str: + """Extract session ID from cookie or query param.""" + sid = request.cookies.get("vm_bench_sid", "") + if not sid: + sid = request.query_params.get("session_id", "") + return sid + api = ApiClient() logger = logging.getLogger("vm-bench") @@ -141,9 +152,12 @@ def session_upload( source_type: str = Form("upload"), source_file: Optional[UploadFile] = File(None), source_url: Optional[str] = Form(None), + session_id: str = Form(""), ): """Phase 1 — acquire the source file. Returns JSON so the frontend can show progress, then call /session/analyze separately.""" + staging = _staging(session_id) + staging.mkdir(parents=True, exist_ok=True) err = _validate_vmid(vmid) if err: return JSONResponse({"phase": "error", "error": err}, status_code=400) @@ -156,7 +170,7 @@ def session_upload( return JSONResponse({"phase": "error", "error": "No file uploaded."}, status_code=400) filename = source_file.filename - dest = STAGING / filename + dest = staging / filename content_length = request.headers.get("content-length") if content_length: @@ -203,11 +217,11 @@ def session_upload( 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 + dest = staging / filename _active_downloads.pop(filename, None) - usage = shutil.disk_usage(STAGING) + 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) @@ -254,6 +268,7 @@ async def upload_raw(request: Request): filename = request.headers.get("X-Filename", "upload.bin") vmid_str = request.headers.get("X-VMID", "") vm_name = request.headers.get("X-VM-Name", "") + session_id = request.headers.get("X-Session-ID", "") try: vmid = int(vmid_str) @@ -264,7 +279,9 @@ async def upload_raw(request: Request): if err: return JSONResponse({"phase": "error", "error": err}, status_code=400) - dest = STAGING / filename + staging = _staging(session_id) + staging.mkdir(parents=True, exist_ok=True) + dest = staging / filename content_length = request.headers.get("content-length") if content_length: estimated_gb = int(content_length) / (1024**3) @@ -445,6 +462,7 @@ async def confirm_session( target_disk_size_gb: Optional[int] = Form(None), auto_detect_boot: str = Form("true"), boot_type: str = Form("uefi"), + session_id: str = Form(""), ): """Build the job payload and submit to the backend.""" # Validate VM ID range @@ -460,6 +478,7 @@ async def confirm_session( "target_storage": target_storage, "auto_detect_boot": auto_detect_boot == "true", "boot_type": boot_type, + "session_id": session_id, "boot_disk": { "disk_type": "image_file", "source_filename": source_filename, diff --git a/frontend/templates/base.html b/frontend/templates/base.html index 00f03a4..29d7b33 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -5,6 +5,22 @@