hermes-sshx-link/dashboard/plugin_api.py

137 lines
3.7 KiB
Python
Raw Normal View History

"""
sshx-link dashboard backend
===========================
FastAPI routes for starting sshx and returning the share link.
"""
import asyncio
import subprocess
import re
import shutil
import logging
from fastapi import APIRouter
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
router = APIRouter()
# Keep track of running sshx process
_sshx_process = None
_sshx_link = None
def _find_link(text: str) -> str | None:
"""Extract sshx link from output."""
match = re.search(r"https://sshx\.io/s/[A-Za-z0-9_-]+", text)
return match.group(0) if match else None
def _ensure_sshx() -> bool:
"""Install sshx if not present."""
if shutil.which("sshx"):
return True
try:
subprocess.run(
["sh", "-c", "curl -sSf https://sshx.io/get | sh"],
capture_output=True,
timeout=60,
)
return shutil.which("sshx") is not None
except Exception as e:
logger.error("sshx install failed: %s", e)
return False
@router.get("/start")
async def start_sshx():
"""Start sshx serve and return the share link."""
global _sshx_process, _sshx_link
# Already running
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
return {"status": "running", "link": _sshx_link}
# Install if needed
if not _ensure_sshx():
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Failed to install sshx"},
)
# Kill old process if any
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
_sshx_process.wait(timeout=5)
# Start sshx serve
_sshx_process = subprocess.Popen(
["sshx", "serve"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
_sshx_link = None
# Read output in background thread, await link
loop = asyncio.get_event_loop()
link_future = loop.create_future()
def _reader():
global _sshx_link
for line in _sshx_process.stdout:
line = line.strip()
link = _find_link(line)
if link:
_sshx_link = link
if not link_future.done():
link_future.set_result(link)
return
if not link_future.done():
link_future.set_exception RuntimeError("sshx exited without link")
import threading
threading.Thread(target=_reader, daemon=True).start()
try:
link = await asyncio.wait_for(link_future, timeout=30)
return {"status": "running", "link": link}
except asyncio.TimeoutError:
return JSONResponse(
status_code=504,
content={"status": "error", "message": "sshx did not return a link in time"},
)
except Exception as e:
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)},
)
@router.get("/status")
async def sshx_status():
"""Check if sshx is running and return the link."""
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None and _sshx_link:
return {"status": "running", "link": _sshx_link}
return {"status": "stopped", "link": None}
@router.post("/stop")
async def stop_sshx():
"""Stop the running sshx process."""
global _sshx_process, _sshx_link
if _sshx_process and _sshx_process.poll() is None:
_sshx_process.terminate()
try:
_sshx_process.wait(timeout=5)
except subprocess.TimeoutExpired:
_sshx_process.kill()
_sshx_process = None
_sshx_link = None
return {"status": "stopped"}
return {"status": "already_stopped"}