new changes

This commit is contained in:
Niranjan
2026-04-07 04:24:16 +05:30
parent 8bba285f56
commit 464ff188ad
2 changed files with 264 additions and 45 deletions

View File

@@ -1,8 +1,12 @@
"""YakPanel - App Store / Software API""" """YakPanel - App Store / Software API"""
from __future__ import annotations
import asyncio import asyncio
import os import os
import shlex import shlex
import shutil
import subprocess import subprocess
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@@ -12,25 +16,147 @@ from app.models.user import User
router = APIRouter(prefix="/soft", tags=["soft"]) router = APIRouter(prefix="/soft", tags=["soft"])
# Curated list of common server software (Debian/Ubuntu package names) PmKind = Literal["apt", "dnf", "yum", "apk", "none"]
SOFTWARE_LIST = [
{"id": "nginx", "name": "Nginx", "desc": "Web server", "pkg": "nginx"}, # Per distro: apt = Debian/Ubuntu, rpm = RHEL/Fedora/Alma/Rocky, apk = Alpine (best-effort)
{"id": "mysql-server", "name": "MySQL Server", "desc": "Database server", "pkg": "mysql-server"}, SOFTWARE_LIST: list[dict[str, str]] = [
{"id": "mariadb-server", "name": "MariaDB", "desc": "Database server", "pkg": "mariadb-server"}, {
{"id": "php", "name": "PHP", "desc": "PHP runtime", "pkg": "php"}, "id": "nginx",
{"id": "php-fpm", "name": "PHP-FPM", "desc": "PHP FastCGI", "pkg": "php-fpm"}, "name": "Nginx",
{"id": "redis-server", "name": "Redis", "desc": "In-memory cache", "pkg": "redis-server"}, "desc": "Web server",
{"id": "postgresql", "name": "PostgreSQL", "desc": "Database server", "pkg": "postgresql"}, "apt": "nginx",
{"id": "mongodb", "name": "MongoDB", "desc": "NoSQL database", "pkg": "mongodb"}, "rpm": "nginx",
{"id": "certbot", "name": "Certbot", "desc": "Let's Encrypt SSL", "pkg": "certbot"}, "apk": "nginx",
{"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": "mysql-server",
{"id": "git", "name": "Git", "desc": "Version control", "pkg": "git"}, "name": "MySQL Server",
{"id": "python3", "name": "Python 3", "desc": "Python runtime", "pkg": "python3"}, "desc": "Database server",
"apt": "mysql-server",
"rpm": "mysql-server",
"apk": "mysql",
},
{
"id": "mariadb-server",
"name": "MariaDB",
"desc": "Database server",
"apt": "mariadb-server",
"rpm": "mariadb-server",
"apk": "mariadb",
},
{
"id": "php",
"name": "PHP",
"desc": "PHP runtime",
"apt": "php",
"rpm": "php",
"apk": "php",
},
{
"id": "php-fpm",
"name": "PHP-FPM",
"desc": "PHP FastCGI",
"apt": "php-fpm",
"rpm": "php-fpm",
"apk": "php-fpm",
},
{
"id": "redis-server",
"name": "Redis",
"desc": "In-memory cache",
"apt": "redis-server",
"rpm": "redis",
"apk": "redis",
},
{
"id": "postgresql",
"name": "PostgreSQL",
"desc": "Database server",
"apt": "postgresql",
"rpm": "postgresql-server",
"apk": "postgresql",
},
{
"id": "mongodb",
"name": "MongoDB",
"desc": "NoSQL database",
"apt": "mongodb",
"rpm": "mongodb-server",
"apk": "mongodb",
},
{
"id": "certbot",
"name": "Certbot",
"desc": "Let's Encrypt SSL",
"apt": "certbot",
"rpm": "certbot",
"apk": "certbot",
},
{
"id": "docker",
"name": "Docker",
"desc": "Container runtime",
"apt": "docker.io",
"rpm": "docker",
"apk": "docker",
},
{
"id": "nodejs",
"name": "Node.js",
"desc": "JavaScript runtime",
"apt": "nodejs",
"rpm": "nodejs",
"apk": "nodejs",
},
{
"id": "npm",
"name": "npm",
"desc": "Node package manager",
"apt": "npm",
"rpm": "npm",
"apk": "npm",
},
{
"id": "git",
"name": "Git",
"desc": "Version control",
"apt": "git",
"rpm": "git",
"apk": "git",
},
{
"id": "python3",
"name": "Python 3",
"desc": "Python runtime",
"apt": "python3",
"rpm": "python3",
"apk": "python3",
},
] ]
def _detect_package_manager() -> PmKind:
if shutil.which("apt-get"):
return "apt"
if shutil.which("dnf"):
return "dnf"
if shutil.which("yum"):
return "yum"
if shutil.which("apk"):
return "apk"
return "none"
def _package_name(entry: dict[str, str], pm: PmKind) -> str:
if pm == "apt":
return entry["apt"]
if pm in ("dnf", "yum"):
return entry["rpm"]
if pm == "apk":
return entry.get("apk") or entry["apt"]
return entry["apt"]
def _apt_env() -> dict[str, str]: def _apt_env() -> dict[str, str]:
env = os.environ.copy() env = os.environ.copy()
env.setdefault("DEBIAN_FRONTEND", "noninteractive") env.setdefault("DEBIAN_FRONTEND", "noninteractive")
@@ -38,15 +164,14 @@ def _apt_env() -> dict[str, str]:
return env return env
def _run_apt_shell(script: str, timeout: int) -> None: def _run_shell(script: str, timeout: int, env: dict[str, str] | None = None) -> None:
"""Run apt/dpkg shell snippet; raise HTTPException if command fails."""
result = subprocess.run( result = subprocess.run(
script, script,
shell=True, shell=True,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=timeout, timeout=timeout,
env=_apt_env(), env=env if env is not None else os.environ.copy(),
) )
if result.returncode == 0: if result.returncode == 0:
return return
@@ -56,29 +181,113 @@ def _run_apt_shell(script: str, timeout: int) -> None:
raise HTTPException(status_code=500, detail=msg[:4000]) raise HTTPException(status_code=500, detail=msg[:4000])
def _check_installed(pkg: str) -> tuple[bool, str]: def _check_installed_apt(pkg: str) -> tuple[bool, str]:
"""Check if package is installed. Returns (installed, version_or_error).""" out, _ = exec_shell_sync(f"dpkg -l {shlex.quote(pkg)} 2>/dev/null | grep ^ii", timeout=5)
out, err = exec_shell_sync(f"dpkg -l {pkg} 2>/dev/null | grep ^ii", timeout=5)
if out.strip(): if out.strip():
# Parse version from dpkg output: ii pkg version ...
parts = out.split() parts = out.split()
if len(parts) >= 3: if len(parts) >= 3:
return True, parts[2] return True, parts[2]
return False, "" return False, ""
def _check_installed_rpm(pkg: str) -> tuple[bool, str]:
try:
result = subprocess.run(
["rpm", "-q", "--queryformat", "%{EVR}", pkg],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
return False, ""
ver = (result.stdout or "").strip()
return (True, ver) if ver else (True, "")
except (FileNotFoundError, subprocess.TimeoutExpired):
return False, ""
def _check_installed_apk(pkg: str) -> tuple[bool, str]:
try:
r = subprocess.run(
["apk", "info", "-e", pkg],
capture_output=True,
text=True,
timeout=5,
)
if r.returncode != 0:
return False, ""
out = (r.stdout or "").strip()
if not out:
return True, ""
first = out.split("\n")[0].strip()
return True, first
except (FileNotFoundError, subprocess.TimeoutExpired):
return False, ""
def _check_installed(pm: PmKind, pkg: str) -> tuple[bool, str]:
if pm == "apt":
return _check_installed_apt(pkg)
if pm in ("dnf", "yum"):
return _check_installed_rpm(pkg)
if pm == "apk":
return _check_installed_apk(pkg)
return False, ""
def _install_script(pm: PmKind, pkg: str) -> tuple[str, int, dict[str, str] | None]:
q = shlex.quote(pkg)
if pm == "apt":
return f"apt-get update -qq && apt-get install -y {q}", 600, _apt_env()
if pm == "dnf":
return f"dnf install -y {q}", 600, None
if pm == "yum":
return f"yum install -y {q}", 600, None
if pm == "apk":
return f"apk update && apk add {q}", 600, None
raise HTTPException(
status_code=501,
detail="No supported package manager found (need apt-get, dnf, yum, or apk).",
)
def _uninstall_script(pm: PmKind, pkg: str) -> tuple[str, int, dict[str, str] | None]:
q = shlex.quote(pkg)
if pm == "apt":
return f"apt-get remove -y {q}", 180, _apt_env()
if pm == "dnf":
return f"dnf remove -y {q}", 180, None
if pm == "yum":
return f"yum remove -y {q}", 180, None
if pm == "apk":
return f"apk del {q}", 120, None
raise HTTPException(
status_code=501,
detail="No supported package manager found (need apt-get, dnf, yum, or apk).",
)
@router.get("/list") @router.get("/list")
async def soft_list(current_user: User = Depends(get_current_user)): async def soft_list(current_user: User = Depends(get_current_user)):
"""List software with install status""" """List software with install status for this OS."""
result = [] pm = _detect_package_manager()
manager_label = pm if pm != "none" else "unknown"
result: list[dict] = []
for s in SOFTWARE_LIST: for s in SOFTWARE_LIST:
installed, version = _check_installed(s["pkg"]) pkg = _package_name(s, pm) if pm != "none" else s["apt"]
result.append({ installed, version = _check_installed(pm, pkg) if pm != "none" else (False, "")
**s, result.append(
{
"id": s["id"],
"name": s["name"],
"desc": s["desc"],
"pkg": pkg,
"installed": installed, "installed": installed,
"version": version if installed else "", "version": version if installed else "",
}) "package_manager": manager_label,
return {"software": result} }
)
return {"software": result, "package_manager": manager_label}
@router.post("/install/{pkg_id}") @router.post("/install/{pkg_id}")
@@ -86,12 +295,14 @@ async def soft_install(
pkg_id: str, pkg_id: str,
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Install package via apt (requires root)""" """Install package via system package manager (root privileges required)."""
pkg = next((s["pkg"] for s in SOFTWARE_LIST if s["id"] == pkg_id), None) pm = _detect_package_manager()
if not pkg: entry = next((s for s in SOFTWARE_LIST if s["id"] == pkg_id), None)
if not entry:
raise HTTPException(status_code=404, detail="Package not found") raise HTTPException(status_code=404, detail="Package not found")
quoted = shlex.quote(pkg) pkg = _package_name(entry, pm)
_run_apt_shell(f"apt-get update -qq && apt-get install -y {quoted}", timeout=600) script, timeout, env = _install_script(pm, pkg)
await asyncio.to_thread(_run_shell, script, timeout, env)
return {"status": True, "msg": "Installed"} return {"status": True, "msg": "Installed"}
@@ -100,10 +311,12 @@ async def soft_uninstall(
pkg_id: str, pkg_id: str,
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Uninstall package via apt""" """Uninstall package via system package manager."""
pkg = next((s["pkg"] for s in SOFTWARE_LIST if s["id"] == pkg_id), None) pm = _detect_package_manager()
if not pkg: entry = next((s for s in SOFTWARE_LIST if s["id"] == pkg_id), None)
if not entry:
raise HTTPException(status_code=404, detail="Package not found") raise HTTPException(status_code=404, detail="Package not found")
quoted = shlex.quote(pkg) pkg = _package_name(entry, pm)
await asyncio.to_thread(_run_apt_shell, f"apt-get remove -y {quoted}", 180) script, timeout, env = _uninstall_script(pm, pkg)
await asyncio.to_thread(_run_shell, script, timeout, env)
return {"status": True, "msg": "Uninstalled"} return {"status": True, "msg": "Uninstalled"}

View File

@@ -9,6 +9,7 @@ interface Software {
pkg: string pkg: string
installed: boolean installed: boolean
version: string version: string
package_manager?: string
} }
export function SoftPage() { export function SoftPage() {
@@ -16,11 +17,15 @@ export function SoftPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
const [actionId, setActionId] = useState<string | null>(null) const [actionId, setActionId] = useState<string | null>(null)
const [detectedPm, setDetectedPm] = useState<string>('')
const load = () => { const load = () => {
setLoading(true) setLoading(true)
apiRequest<{ software: Software[] }>('/soft/list') apiRequest<{ software: Software[]; package_manager?: string }>('/soft/list')
.then((data) => setSoftware(data.software || [])) .then((data) => {
setSoftware(data.software || [])
setDetectedPm(data.package_manager || '')
})
.catch((err) => setError(err.message)) .catch((err) => setError(err.message))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
} }
@@ -69,7 +74,8 @@ export function SoftPage() {
)} )}
<div className="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200 text-sm"> <div className="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200 text-sm">
Install/uninstall via apt. Panel must run with sufficient privileges. Target: Debian/Ubuntu. Installs use your server package manager ({detectedPm || '…loading…'}). Panel must run as root
(or equivalent). Supported: Debian/Ubuntu (apt), RHEL/Fedora/Alma/Rocky (dnf/yum), Alpine (apk).
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">