Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""Резервное копирование: 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)
|
||||
Reference in New Issue
Block a user