import json import logging import os import asyncssh from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Query import config router = APIRouter() logger = logging.getLogger(__name__) # Absolute paths to media-mgmt scripts on NAS host HEALTH_CHECK_SCRIPT = "/volume1/homes/zjgump/media-mgmt/immich/scripts/health-check.sh" STORAGE_REPORT_SCRIPT = "/volume1/homes/zjgump/media-mgmt/diagnostics/storage-report.sh" SCAN_LIBRARY_SCRIPT = "/volume1/homes/zjgump/media-mgmt/immich/scripts/scan-library.sh" THUMBNAIL_CLINIC_SCRIPT = "/volume1/homes/zjgump/media-mgmt/diagnostics/thumbnail-clinic.sh" SHUTTLE_SCRIPT = "/volume1/homes/zjgump/media-mgmt/transfer/ssd-shuttle.sh" PHONE_INGEST_SCRIPT = "/volume1/homes/zjgump/media-mgmt/transfer/phone-to-nas.sh" LOG_DIR = "/volume1/homes/zjgump/media-mgmt/logs" async def run_host_command(cmd: str) -> tuple[int, str, str]: """Execute a command on the NAS host via SSH.""" try: # Prepend PATH and NAS_HOST variables for host execution full_cmd = f'PATH="/volume1/homes/zjgump/bin:$PATH" NAS_HOST="naslan" {cmd}' async with asyncssh.connect( config.SSH_HOST, username=config.SSH_USER, client_keys=[config.SSH_KEY_PATH], known_hosts=None ) as conn: result = await conn.run(full_cmd) return result.exit_status, result.stdout, result.stderr except Exception as e: logger.error(f"SSH command failed ({cmd}): {e}") return 999, "", str(e) @router.get("/health") async def get_health(): """Runs health-check.sh and returns the exit status and stdout/stderr.""" code, stdout, stderr = await run_host_command(f"bash {HEALTH_CHECK_SCRIPT}") return { "status": "ok" if code == 0 else "warning" if code == 1 else "error", "exit_code": code, "stdout": stdout, "stderr": stderr } @router.get("/storage") async def get_storage(): """Runs storage-report.sh -j and returns the parsed JSON storage report.""" code, stdout, stderr = await run_host_command(f"bash {STORAGE_REPORT_SCRIPT} -j") if code != 0: raise HTTPException(status_code=500, detail=f"Storage report failed: {stderr}") try: return json.loads(stdout) except json.JSONDecodeError: return { "error": "Failed to parse JSON from storage report", "raw_output": stdout, "stderr": stderr } @router.post("/maintenance/scan") async def run_scan(background_tasks: BackgroundTasks, library: str = Query(None)): """Triggers scan-library.sh in the background and writes stdout/stderr to scan.log.""" cmd = f"bash {SCAN_LIBRARY_SCRIPT}" if library: cmd += f" {library}" log_file = f"{LOG_DIR}/scan.log" run_cmd = f"mkdir -p {LOG_DIR} && echo '=== Scan Started at $(date) ===' > {log_file} && nohup {cmd} >> {log_file} 2>&1 &" code, stdout, stderr = await run_host_command(run_cmd) if code != 0: raise HTTPException(status_code=500, detail=f"Failed to start scan: {stderr}") return {"message": "Scan started in background", "log_file": log_file} @router.post("/maintenance/requeue") async def run_requeue(background_tasks: BackgroundTasks): """Triggers thumbnail-clinic.sh -r in the background and writes stdout/stderr to requeue.log.""" cmd = f"bash {THUMBNAIL_CLINIC_SCRIPT} -r" log_file = f"{LOG_DIR}/requeue.log" run_cmd = f"mkdir -p {LOG_DIR} && echo '=== Requeue Started at $(date) ===' > {log_file} && nohup {cmd} >> {log_file} 2>&1 &" code, stdout, stderr = await run_host_command(run_cmd) if code != 0: raise HTTPException(status_code=500, detail=f"Failed to start thumbnail requeue: {stderr}") return {"message": "Thumbnail requeue started in background", "log_file": log_file} @router.get("/logs/{task_name}") async def get_logs(task_name: str): """Reads logs for a task from /volume1/homes/zjgump/media-mgmt/logs/{task_name}.log.""" if task_name not in ["scan", "requeue", "shuttle", "phone-ingest"]: raise HTTPException(status_code=400, detail="Invalid task name") log_path = f"/volume1/homes/zjgump/media-mgmt/logs/{task_name}.log" if not os.path.exists(log_path): return {"content": "No log file found."} try: with open(log_path, "r") as f: content = f.readlines() return {"content": "".join(content[-200:])} except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to read log file: {e}") @router.get("/status") async def get_active_status(): """Checks if any ingestion/maintenance processes are currently running on the host.""" processes = { "scan": SCAN_LIBRARY_SCRIPT, "requeue": THUMBNAIL_CLINIC_SCRIPT, "shuttle": SHUTTLE_SCRIPT, "phone-ingest": PHONE_INGEST_SCRIPT } status = {} for name, script_path in processes.items(): code, stdout, stderr = await run_host_command(f"pgrep -f {script_path}") status[name] = { "running": code == 0, "pid": stdout.strip() if code == 0 else None } return status