f624fee5e0
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""Резервное копирование: 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)
|