diff --git a/app/main.py b/app/main.py index 020d0a6..40e0eb5 100644 --- a/app/main.py +++ b/app/main.py @@ -1074,5 +1074,137 @@ def get_upload_file(filename: str): raise HTTPException(status_code=404, detail="File not found.") return FileResponse(file_path) +class CleanupTempRequest(BaseModel): + paths: List[str] | None = None + +def get_dir_stats(path: str) -> tuple[int, int]: + total_size = 0 + total_files = 0 + if not os.path.exists(path): + return 0, 0 + for root, dirs, files in os.walk(path): + for f in files: + fp = os.path.join(root, f) + try: + if os.path.exists(fp): + total_size += os.path.getsize(fp) + total_files += 1 + except Exception: + pass + return total_size, total_files + +@app.get("/api/system/temp-info") +def get_temp_info(): + temp_folders = [] + + # 1. Collect all potential temp root directories + # Default temp root + temp_roots = {upscaler.TEMP_DIR} + + # Custom temp roots from jobs db + for job in jobs_db.values(): + if getattr(job, "temp_dir", None): + temp_roots.add(job.temp_dir) + + # 2. Scan folders inside the temp roots + seen_paths = set() + for root_dir in temp_roots: + if not os.path.exists(root_dir): + continue + try: + for item in os.listdir(root_dir): + item_path = os.path.join(root_dir, item) + if not os.path.isdir(item_path): + continue + if item_path in seen_paths: + continue + seen_paths.add(item_path) + + # Check if it corresponds to a job_id (which is usually a UUID) + job_id = item + job = jobs_db.get(job_id) + + size_bytes, file_count = get_dir_stats(item_path) + + if job: + job_status = job.status + video_name = os.path.basename(job.video_path) + else: + job_status = "orphaned" + video_name = "Unknown Video (Orphaned)" + + temp_folders.append({ + "job_id": job_id, + "path": item_path, + "size_bytes": size_bytes, + "file_count": file_count, + "status": job_status, + "video_name": video_name + }) + except Exception as e: + print(f"Error scanning temp root {root_dir}: {e}") + + return {"temp_folders": temp_folders} + +@app.post("/api/system/temp-cleanup") +def cleanup_temp(req: CleanupTempRequest): + import shutil + cleaned = [] + errors = [] + + # Identify active job IDs + active_statuses = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"] + active_job_ids = {job.job_id for job in jobs_db.values() if job.status in active_statuses} + + # Get current temp folder status + temp_info = get_temp_info() + folders = temp_info["temp_folders"] + + target_paths = req.paths if req.paths is not None else [f["path"] for f in folders] + + for f in folders: + path = f["path"] + if path not in target_paths: + continue + + # Safety checks: + if f["job_id"] in active_job_ids: + errors.append(f"Cannot clean up active job {f['job_id']}") + continue + + resolved_path = os.path.realpath(path) + is_sub_of_temp = resolved_path.startswith(os.path.realpath(upscaler.TEMP_DIR)) + + if not is_sub_of_temp: + for job in jobs_db.values(): + if getattr(job, "temp_dir", None): + if resolved_path.startswith(os.path.realpath(job.temp_dir)): + is_sub_of_temp = True + break + + if not is_sub_of_temp: + errors.append(f"Safety constraint: Path {path} is not in a valid temp root.") + continue + + try: + if os.path.exists(path): + shutil.rmtree(path) + cleaned.append(path) + + # Update DB state if needed + job = jobs_db.get(f["job_id"]) + if job and job.status in ["paused", "interrupted", "failed"]: + job.status = "interrupted" + job.progress = 0.0 + job.current_frame = 0 + job.eta = "Temp files cleaned. Will restart on resume." + else: + cleaned.append(path) + except Exception as e: + errors.append(f"Error removing {path}: {str(e)}") + + save_jobs_db() + return {"cleaned": cleaned, "errors": errors} + # Mount static folder app.mount("/", StaticFiles(directory=os.path.join(upscaler.BASE_DIR, "static"), html=True), name="static") diff --git a/static/app.js b/static/app.js index 96c6ebb..28a2ea3 100644 --- a/static/app.js +++ b/static/app.js @@ -2084,6 +2084,140 @@ async function initializeApp() { } }); } + + // Temporary storage maintenance handlers + const refreshTempBtn = document.getElementById("refresh-temp-btn"); + if (refreshTempBtn) { + refreshTempBtn.addEventListener("click", loadTempInfo); + } + + const cleanupTempBtn = document.getElementById("cleanup-temp-btn"); + if (cleanupTempBtn) { + cleanupTempBtn.addEventListener("click", async () => { + if (confirm("Are you sure you want to clean up all inactive temporary directories?\nThis will clear any completed, cancelled, failed, or orphaned job frames. Paused or interrupted jobs will have to restart from the beginning on resume.")) { + try { + const cleanRes = await fetch("/api/system/temp-cleanup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ paths: null }) + }); + if (cleanRes.ok) { + loadTempInfo(); + loadQueue(); + } else { + alert("Cleanup failed."); + } + } catch (err) { + console.error(err); + } + } + }); + } + + // Refresh queue button should also refresh temp files info + const refreshQueueBtn = document.getElementById("refresh-queue-btn"); + if (refreshQueueBtn) { + refreshQueueBtn.addEventListener("click", loadTempInfo); + } + + // Load initial temp info + loadTempInfo(); +} + +async function loadTempInfo() { + const listContainer = document.getElementById("temp-files-list"); + const totalSizeBadge = document.getElementById("temp-total-size"); + const folderCountBadge = document.getElementById("temp-folder-count"); + if (!listContainer) return; + + try { + const res = await fetch("/api/system/temp-info"); + if (!res.ok) return; + const data = await res.json(); + const folders = data.temp_folders || []; + + let totalSize = 0; + folders.forEach(f => totalSize += (f.size_bytes || 0)); + + if (totalSizeBadge) totalSizeBadge.textContent = formatBytes(totalSize); + if (folderCountBadge) folderCountBadge.textContent = folders.length; + + if (folders.length === 0) { + listContainer.innerHTML = ` +
+

No temporary directories found.

+
+ `; + return; + } + + listContainer.innerHTML = ""; + + folders.forEach(f => { + const item = document.createElement("div"); + item.className = "queue-item"; + + const sizeStr = formatBytes(f.size_bytes || 0); + const isActive = ["analyzing", "extracting", "upscaling", "restoring_faces", "interpolating", "assembling"].includes(f.status); + + let statusLabel = f.status.toUpperCase(); + let statusClass = f.status; + if (f.status === "orphaned") { + statusLabel = "Orphaned (No Job)"; + statusClass = "interrupted"; + } + + item.innerHTML = ` +
+
+
+ ${f.path} +
+
+ Video: ${f.video_name} + Size: ${sizeStr} (${f.file_count} files) +
+
+
+ ${statusLabel} +
+ +
+
+
+ `; + + const deleteBtn = item.querySelector(".btn-delete-temp"); + if (deleteBtn && !isActive) { + deleteBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + if (confirm(`Are you sure you want to delete this temporary folder?\nPath: ${f.path}`)) { + try { + const cleanRes = await fetch("/api/system/temp-cleanup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ paths: [f.path] }) + }); + if (cleanRes.ok) { + loadTempInfo(); + loadQueue(); + } else { + alert("Failed to delete temp folder."); + } + } catch (err) { + console.error(err); + } + } + }); + } + + listContainer.appendChild(item); + }); + } catch (err) { + console.error("Error loading temp files info:", err); + } } initializeApp(); diff --git a/static/index.html b/static/index.html index 6525001..8021abb 100644 --- a/static/index.html +++ b/static/index.html @@ -564,6 +564,42 @@ + +
+
+

Temporary Storage & Maintenance

+
+ + +
+
+

+ Monitor temporary image frames created during upscaling. Running jobs cannot be cleared. +

+ +
+
+ Total Temp Size + 0 Bytes +
+
+ Temp Folders Count + 0 +
+
+ +
+ +
+

No temporary directories found.

+
+
+
+