"""Резервное копирование: tar на сервере → SFTP → локальный архив.""" from __future__ import annotations import asyncio import shlex from datetime import datetime from pathlib import Path from typing import List, Optional from sqlmodel import select from app.core import files as fcore from app.core.ssh import SSHClient from app.db.database import get_session from app.db.models import BackupRecord, Server BACKUP_DIR = Path.home() / ".debian-manager" / "backups" BACKUP_DIR.mkdir(parents=True, exist_ok=True) async def run_backup(srv: Server, paths: str, download: bool = True, log=None) -> BackupRecord: """Создать tar.gz на сервере и скачать. log — callable(str).""" def _log(line: str): if log: log(line) with get_session() as s: rec = BackupRecord(server_id=srv.id, paths=paths, status="pending") s.add(rec) s.commit() s.refresh(rec) rec_id = rec.id def _set(**fields): with get_session() as s: r = s.get(BackupRecord, rec_id) for k, v in fields.items(): setattr(r, k, v) s.add(r) s.commit() try: ts = datetime.now().strftime("%Y%m%d_%H%M%S") name = f"dm-backup-{srv.name}-{ts}.tar.gz" remote = f"/tmp/{name}" path_args = " ".join(shlex.quote(p) for p in paths.split()) async with SSHClient(srv) as c: _log(f"tar -czf {remote} {path_args}") r = await c.sudo( f"tar -czf {remote} --ignore-failed-read {path_args} 2>&1", timeout=1800 ) if not r.ok and "tar:" not in (r.stdout + r.stderr)[:20]: # tar возвращает 1 при --ignore-failed-read если часть файлов пропущена — ок pass r = await c.sudo(f"stat -c %s {remote}", timeout=30) size = int(r.stdout.strip() or 0) _log(f"архив на сервере: {size // 1024} КБ") # дать доступ пользователю для SFTP await c.sudo(f"chmod 644 {remote} && chown {srv.username}:{srv.username} {remote}") local = "" if download: local_path = BACKUP_DIR / srv.name / name local_path.parent.mkdir(parents=True, exist_ok=True) _log(f"скачивание → {local_path}") await fcore.download(c, remote, str(local_path), lambda d, t: _log(f" {d//1024}/{t//1024} КБ")) local = str(local_path) await c.run(f"rm -f {remote}") _set(status="success", file=local, size=size, log="") rec = get_record(rec_id) return rec except Exception as e: _set(status="error", log=str(e)) return get_record(rec_id) def get_record(rec_id: int) -> BackupRecord: with get_session() as s: return s.get(BackupRecord, rec_id) def list_backups(server_id: Optional[int] = None, limit: int = 50) -> List[BackupRecord]: with get_session() as s: q = select(BackupRecord).order_by(BackupRecord.id.desc()).limit(limit) if server_id: q = q.where(BackupRecord.server_id == server_id) return list(s.exec(q).all()) def stats() -> tuple[int, int]: """(кол-во, суммарный размер байт)""" recs = list_backups(limit=1000) ok = [r for r in recs if r.status == "success"] return len(ok), sum(r.size for r in ok)