diff --git a/.gitignore b/.gitignore index ae9c5fe..4e66f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ -# Staging data — not code -/in/ -/out/ +# Transient session data — not code +/tmp/ /logs/ # Python diff --git a/backend/converter.py b/backend/converter.py index 08c3d09..8417320 100644 --- a/backend/converter.py +++ b/backend/converter.py @@ -15,8 +15,9 @@ from pathlib import Path from dataclasses import dataclass from typing import Optional -STAGING_IN = Path("/mnt/converter/in") -STAGING_OUT = Path("/mnt/converter/out") +STAGING_ROOT = Path("/mnt/converter/tmp") +STAGING_IN = STAGING_ROOT / "in" +STAGING_OUT = STAGING_ROOT / "out" logger = logging.getLogger("backend.converter") @@ -44,8 +45,11 @@ class DiskInfo: def extract_if_needed(filename: str) -> Path: """Extract an archive into STAGING_IN if it's compressed. Returns the path - to the extracted directory (or the original file if not compressed).""" - filepath = STAGING_IN / filename + to the extracted directory (or the original file if not compressed). + Accepts both absolute paths and paths relative to STAGING_IN.""" + filepath = Path(filename) + if not filepath.is_absolute(): + filepath = STAGING_IN / filename if not filepath.exists(): raise FileNotFoundError(f"Source not found: {filepath}") diff --git a/backend/install.sh b/backend/install.sh index 5349127..c2f4d63 100755 --- a/backend/install.sh +++ b/backend/install.sh @@ -61,6 +61,7 @@ for d in \ "$CONVERTER_ROOT/in" \ "$CONVERTER_ROOT/out" \ "$CONVERTER_ROOT/logs" \ + "$CONVERTER_ROOT/tmp" \ "$CONVERTER_ROOT/backend" \ "$CONVERTER_ROOT/frontend" \ "$CONVERTER_ROOT/frontend/templates" \ diff --git a/backend/provisioner.py b/backend/provisioner.py index 5f47dab..d09a029 100644 --- a/backend/provisioner.py +++ b/backend/provisioner.py @@ -30,8 +30,9 @@ from converter import extract_if_needed, discover_disk, detect_efi logger = logging.getLogger("backend.provisioner") -STAGING_IN = Path("/mnt/converter/in") -STAGING_OUT = Path("/mnt/converter/out") +STAGING_ROOT = Path("/mnt/converter/tmp") +STAGING_IN = STAGING_ROOT / "in" +STAGING_OUT = STAGING_ROOT / "out" def _staging_in(session_id: str = "") -> Path: return STAGING_IN / session_id if session_id else STAGING_IN diff --git a/frontend/app.py b/frontend/app.py index 67418ba..eb4668e 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -61,11 +61,11 @@ BASE = Path(__file__).parent app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static") _jinja = Environment(loader=FileSystemLoader(str(BASE / "templates")), autoescape=True) -STAGING = Path("/mnt/converter/in") +STAGING_ROOT = Path("/mnt/converter/tmp") def _staging(session_id: str = "") -> Path: - """Session-aware staging directory.""" - return STAGING / session_id if session_id else STAGING + """Session-aware staging directory. Returns /mnt/converter/tmp/{guid}/ or /mnt/converter/tmp/""" + return STAGING_ROOT / session_id if session_id else STAGING_ROOT def _get_session_id(request: Request) -> str: """Extract session ID from cookie or query param.""" @@ -156,8 +156,8 @@ def session_upload( ): """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) + sdir = _staging(session_id) / "in" + sdir.mkdir(parents=True, exist_ok=True) err = _validate_vmid(vmid) if err: return JSONResponse({"phase": "error", "error": err}, status_code=400) @@ -170,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 = sdir / filename content_length = request.headers.get("content-length") if content_length: @@ -217,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 = sdir / filename _active_downloads.pop(filename, None) - usage = shutil.disk_usage(staging) + usage = shutil.disk_usage(_staging(session_id)) free_gb = usage.free / (1024**3) if free_gb < 50: logger.warning("Low disk: %.1f GB free — download may fail", free_gb) @@ -279,9 +279,9 @@ async def upload_raw(request: Request): if err: return JSONResponse({"phase": "error", "error": err}, status_code=400) - staging = _staging(session_id) - staging.mkdir(parents=True, exist_ok=True) - dest = staging / filename + sdir = _staging(session_id) / "in" + sdir.mkdir(parents=True, exist_ok=True) + dest = sdir / filename content_length = request.headers.get("content-length") if content_length: estimated_gb = int(content_length) / (1024**3) @@ -322,7 +322,7 @@ async def session_progress(filename: str): if not info: # Check if file exists on disk (download already completed in a # previous session, or it was an upload) - dest = STAGING / filename + dest = _staging("") / "in" / filename if dest.exists(): return JSONResponse({ "phase": "complete", @@ -572,9 +572,9 @@ def scp_start( if not filename: return JSONResponse({"phase": "error", "error": "Invalid remote path."}, status_code=400) - session_dir = STAGING / session_id if session_id else STAGING - session_dir.mkdir(parents=True, exist_ok=True) - dest = session_dir / filename + sdir = _staging(session_id) / "in" + sdir.mkdir(parents=True, exist_ok=True) + dest = sdir / filename # Check disk space usage = shutil.disk_usage(STAGING) @@ -620,7 +620,7 @@ def scp_progress(session_id: str, filename: str): key = f"{session_id}/{filename}" info = _active_scp.get(key) if not info: - dest = STAGING / session_id / filename + dest = _staging(session_id) / "in" / filename if dest.exists(): return JSONResponse({ "phase": "complete",