"""APT: update/upgrade, списки пакетов, установка/удаление.""" from __future__ import annotations from dataclasses import dataclass from typing import Dict, List from app.core.ssh import SSHClient SYSTEM_PRIORITIES = {"required", "important"} @dataclass class Package: name: str version: str priority: str manual: bool upgradable: bool = False candidate: str = "" @property def is_system(self) -> bool: return self.priority in SYSTEM_PRIORITIES async def apt_update(c: SSHClient) -> str: r = await c.sudo("DEBIAN_FRONTEND=noninteractive apt-get update", timeout=600) return r.stdout + r.stderr async def list_upgradable(c: SSHClient) -> List[dict]: r = await c.run("apt list --upgradable 2>/dev/null | tail -n +2") out = [] for line in r.stdout.splitlines(): if not line.strip(): continue # nginx/stable 1.22.1-9+deb12u1 amd64 [upgradable from: 1.22.1-9] parts = line.split() if len(parts) >= 2: name = parts[0].split("/")[0] out.append({"name": name, "candidate": parts[1], "raw": line}) return out async def list_packages(c: SSHClient) -> List[Package]: """Все установленные пакеты с приоритетом и признаком manual.""" r = await c.run( "dpkg-query -W -f='${Package}\\t${Version}\\t${Priority}\\n'", timeout=180 ) manual_r = await c.run("apt-mark showmanual", timeout=120) manual = set(manual_r.stdout.split()) pkgs: List[Package] = [] for line in r.stdout.splitlines(): parts = line.split("\t") if len(parts) == 3: name, version, prio = parts pkgs.append(Package(name, version, prio, name in manual)) return pkgs def split_packages(pkgs: List[Package]) -> Dict[str, List[Package]]: """Разбивка на системные / пользовательские / зависимости.""" system = [p for p in pkgs if p.is_system] user = [p for p in pkgs if not p.is_system and p.manual] deps = [p for p in pkgs if not p.is_system and not p.manual] return {"system": system, "user": user, "deps": deps} async def apt_upgrade(c: SSHClient, dist: bool = False): """Стриминг upgrade. Возвращает async-итератор строк.""" cmd = "DEBIAN_FRONTEND=noninteractive apt-get {} -y".format( "dist-upgrade" if dist else "upgrade" ) return c.run_stream(cmd, use_sudo=True) async def install(c: SSHClient, names: List[str]): cmd = "DEBIAN_FRONTEND=noninteractive apt-get install -y " + " ".join(names) return c.run_stream(cmd, use_sudo=True) async def remove(c: SSHClient, names: List[str], purge: bool = False): action = "purge" if purge else "remove" cmd = f"DEBIAN_FRONTEND=noninteractive apt-get {action} -y " + " ".join(names) return c.run_stream(cmd, use_sudo=True) async def autoremove(c: SSHClient): return c.run_stream( "DEBIAN_FRONTEND=noninteractive apt-get autoremove -y", use_sudo=True ) async def apt_show(c: SSHClient, name: str) -> str: r = await c.run(f"apt-cache show {name} 2>/dev/null | head -40") return r.stdout async def apt_search(c: SSHClient, query: str) -> List[str]: r = await c.run(f"apt-cache search {query} 2>/dev/null | head -50") return [l for l in r.stdout.splitlines() if l.strip()]