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 @@ VM Bench — Proxmox Image Converter + diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 2fb599b..985811c 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -112,10 +112,12 @@ async function startSession(e) { const vmid = document.getElementById('vmid').value; const vmName = document.getElementById('vm-name').value.trim(); const sourceType = document.getElementById('source-type').value; + const sessionId = window.VM_BENCH_SID || ''; const formData = new FormData(); formData.append('vmid', vmid); formData.append('vm_name', vmName); formData.append('source_type', sourceType); + formData.append('session_id', sessionId); if (sourceType === 'upload') { const fileInput = document.getElementById('source-file'); @@ -151,6 +153,8 @@ async function handleUpload(formData) { xhr.setRequestHeader('X-Filename', encodeURIComponent(file.name)); xhr.setRequestHeader('X-VMID', vmid); xhr.setRequestHeader('X-VM-Name', vmName); + xhr.setRequestHeader('X-Session-ID', sessionId); + xhr.setRequestHeader('X-Session-ID', sessionId); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { @@ -233,6 +237,7 @@ async function runAnalysis(vmid, filename, vmName) { setPhase('Step 2/2: Analysing source image...', 0, true); const fd = new FormData(); fd.append('vmid', vmid); fd.append('filename', filename); fd.append('vm_name', vmName); + fd.append('session_id', window.VM_BENCH_SID || ''); try { const resp = await fetch('/session/analyze', { method: 'POST', body: fd }); const html = await resp.text(); @@ -268,6 +273,7 @@ async function submitJob(e) { status.innerHTML = '
Submitting job...'; const formData = new FormData(form); + formData.append('session_id', window.VM_BENCH_SID || ''); try { const resp = await fetch('/session/confirm', { method: 'POST', body: formData }); const html = await resp.text();