2026-04-07 02:04:22 +05:30
|
|
|
"""YakPanel - App Store / Software API"""
|
2026-04-07 04:20:13 +05:30
|
|
|
import asyncio
|
|
|
|
|
import os
|
|
|
|
|
import shlex
|
|
|
|
|
import subprocess
|
|
|
|
|
|
2026-04-07 02:04:22 +05:30
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
|
|
|
|
|
from app.core.utils import exec_shell_sync
|
|
|
|
|
from app.api.auth import get_current_user
|
|
|
|
|
from app.models.user import User
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/soft", tags=["soft"])
|
|
|
|
|
|
|
|
|
|
# Curated list of common server software (Debian/Ubuntu package names)
|
|
|
|
|
SOFTWARE_LIST = [
|
|
|
|
|
{"id": "nginx", "name": "Nginx", "desc": "Web server", "pkg": "nginx"},
|
|
|
|
|
{"id": "mysql-server", "name": "MySQL Server", "desc": "Database server", "pkg": "mysql-server"},
|
|
|
|
|
{"id": "mariadb-server", "name": "MariaDB", "desc": "Database server", "pkg": "mariadb-server"},
|
|
|
|
|
{"id": "php", "name": "PHP", "desc": "PHP runtime", "pkg": "php"},
|
|
|
|
|
{"id": "php-fpm", "name": "PHP-FPM", "desc": "PHP FastCGI", "pkg": "php-fpm"},
|
|
|
|
|
{"id": "redis-server", "name": "Redis", "desc": "In-memory cache", "pkg": "redis-server"},
|
|
|
|
|
{"id": "postgresql", "name": "PostgreSQL", "desc": "Database server", "pkg": "postgresql"},
|
|
|
|
|
{"id": "mongodb", "name": "MongoDB", "desc": "NoSQL database", "pkg": "mongodb"},
|
|
|
|
|
{"id": "certbot", "name": "Certbot", "desc": "Let's Encrypt SSL", "pkg": "certbot"},
|
|
|
|
|
{"id": "docker", "name": "Docker", "desc": "Container runtime", "pkg": "docker.io"},
|
|
|
|
|
{"id": "nodejs", "name": "Node.js", "desc": "JavaScript runtime", "pkg": "nodejs"},
|
|
|
|
|
{"id": "npm", "name": "npm", "desc": "Node package manager", "pkg": "npm"},
|
|
|
|
|
{"id": "git", "name": "Git", "desc": "Version control", "pkg": "git"},
|
|
|
|
|
{"id": "python3", "name": "Python 3", "desc": "Python runtime", "pkg": "python3"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-04-07 04:20:13 +05:30
|
|
|
def _apt_env() -> dict[str, str]:
|
|
|
|
|
env = os.environ.copy()
|
|
|
|
|
env.setdefault("DEBIAN_FRONTEND", "noninteractive")
|
|
|
|
|
env.setdefault("APT_LISTCHANGES_FRONTEND", "none")
|
|
|
|
|
return env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_apt_shell(script: str, timeout: int) -> None:
|
|
|
|
|
"""Run apt/dpkg shell snippet; raise HTTPException if command fails."""
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
script,
|
|
|
|
|
shell=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=timeout,
|
|
|
|
|
env=_apt_env(),
|
|
|
|
|
)
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
return
|
|
|
|
|
out = (result.stdout or "").strip()
|
|
|
|
|
err = (result.stderr or "").strip()
|
|
|
|
|
msg = err or out or f"Command failed (exit {result.returncode})"
|
|
|
|
|
raise HTTPException(status_code=500, detail=msg[:4000])
|
|
|
|
|
|
|
|
|
|
|
2026-04-07 02:04:22 +05:30
|
|
|
def _check_installed(pkg: str) -> tuple[bool, str]:
|
|
|
|
|
"""Check if package is installed. Returns (installed, version_or_error)."""
|
|
|
|
|
out, err = exec_shell_sync(f"dpkg -l {pkg} 2>/dev/null | grep ^ii", timeout=5)
|
|
|
|
|
if out.strip():
|
|
|
|
|
# Parse version from dpkg output: ii pkg version ...
|
|
|
|
|
parts = out.split()
|
|
|
|
|
if len(parts) >= 3:
|
|
|
|
|
return True, parts[2]
|
|
|
|
|
return False, ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/list")
|
|
|
|
|
async def soft_list(current_user: User = Depends(get_current_user)):
|
|
|
|
|
"""List software with install status"""
|
|
|
|
|
result = []
|
|
|
|
|
for s in SOFTWARE_LIST:
|
|
|
|
|
installed, version = _check_installed(s["pkg"])
|
|
|
|
|
result.append({
|
|
|
|
|
**s,
|
|
|
|
|
"installed": installed,
|
|
|
|
|
"version": version if installed else "",
|
|
|
|
|
})
|
|
|
|
|
return {"software": result}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/install/{pkg_id}")
|
|
|
|
|
async def soft_install(
|
|
|
|
|
pkg_id: str,
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Install package via apt (requires root)"""
|
|
|
|
|
pkg = next((s["pkg"] for s in SOFTWARE_LIST if s["id"] == pkg_id), None)
|
|
|
|
|
if not pkg:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Package not found")
|
2026-04-07 04:20:13 +05:30
|
|
|
quoted = shlex.quote(pkg)
|
|
|
|
|
_run_apt_shell(f"apt-get update -qq && apt-get install -y {quoted}", timeout=600)
|
2026-04-07 02:04:22 +05:30
|
|
|
return {"status": True, "msg": "Installed"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/uninstall/{pkg_id}")
|
|
|
|
|
async def soft_uninstall(
|
|
|
|
|
pkg_id: str,
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Uninstall package via apt"""
|
|
|
|
|
pkg = next((s["pkg"] for s in SOFTWARE_LIST if s["id"] == pkg_id), None)
|
|
|
|
|
if not pkg:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Package not found")
|
2026-04-07 04:20:13 +05:30
|
|
|
quoted = shlex.quote(pkg)
|
|
|
|
|
await asyncio.to_thread(_run_apt_shell, f"apt-get remove -y {quoted}", 180)
|
2026-04-07 02:04:22 +05:30
|
|
|
return {"status": True, "msg": "Uninstalled"}
|