Files

99 lines
3.5 KiB
Python
Raw Permalink Normal View History

"""Резервное копирование: 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)