Files
tg-admin-bot/handlers/backup.py

203 lines
5.8 KiB
Python

import asyncio
import json
from datetime import datetime
from aiogram import F
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from app import dp
from auth import is_admin_msg
from keyboards import backup_kb
from lock_utils import acquire_lock, release_lock
from services.queue import enqueue
from services.backup import backup_badge, restore_help
from services.runner import run_cmd
async def cmd_repo_stats(msg: Message):
await msg.answer("⏳ Loading repo stats…", reply_markup=backup_kb)
# --- restore-size stats ---
rc1, raw1 = await run_cmd(
["restic", "stats", "--json"],
use_restic_env=True,
timeout=30
)
if rc1 != 0:
await msg.answer(raw1, reply_markup=backup_kb)
return
restore = json.loads(raw1)
# --- raw-data stats ---
rc2, raw2 = await run_cmd(
["restic", "stats", "--json", "--mode", "raw-data"],
use_restic_env=True,
timeout=30
)
if rc2 != 0:
await msg.answer(raw2, reply_markup=backup_kb)
return
raw = json.loads(raw2)
# --- snapshots count ---
rc3, raw_snaps = await run_cmd(
["restic", "snapshots", "--json"],
use_restic_env=True,
timeout=20
)
snaps = len(json.loads(raw_snaps)) if rc3 == 0 else "n/a"
msg_text = (
"📦 **Repository stats**\n\n"
f"🧉 Snapshots: {snaps}\n"
f"📁 Files: {restore.get('total_file_count', 'n/a')}\n"
f"💽 Logical size: {restore.get('total_size', 0) / (1024**3):.2f} GiB\n"
f"🧱 Stored data: {raw.get('total_pack_size', 0) / (1024**2):.2f} MiB\n"
)
await msg.answer(msg_text, reply_markup=backup_kb, parse_mode="Markdown")
async def cmd_backup_status(msg: Message):
await msg.answer("⏳ Loading snapshots…", reply_markup=backup_kb)
async def worker():
rc, raw = await run_cmd(
["restic", "snapshots", "--json"],
use_restic_env=True,
timeout=30
)
if rc != 0:
await msg.answer(raw, reply_markup=backup_kb)
return
snaps = json.loads(raw)
if not snaps:
await msg.answer("📦 Snapshots: none", reply_markup=backup_kb)
return
snaps.sort(key=lambda s: s["time"], reverse=True)
# --- badge ---
last = snaps[0]
last_time = datetime.fromisoformat(
last["time"].replace("Z", "+00:00")
)
badge = backup_badge(last_time)
# --- buttons ---
rows = []
for s in snaps[:5]:
t = datetime.fromisoformat(
s["time"].replace("Z", "+00:00")
)
rows.append([
InlineKeyboardButton(
text=f"🧉 {s['short_id']} | {t:%Y-%m-%d %H:%M}",
callback_data=f"snap:{s['short_id']}"
)
])
kb = InlineKeyboardMarkup(inline_keyboard=rows)
await msg.answer(
f"📦 Snapshots ({len(snaps)})\n{badge}",
reply_markup=kb
)
asyncio.create_task(worker())
async def cmd_backup_now(msg: Message):
async def job():
if not acquire_lock("backup"):
await msg.answer("⚠️ Backup уже выполняется", reply_markup=backup_kb)
return
await msg.answer("▶️ Backup запущен", reply_markup=backup_kb)
try:
rc, out = await run_cmd(["sudo", "/usr/local/bin/backup.py", "restic-backup"], timeout=6 * 3600)
await msg.answer(("✅ OK\n" if rc == 0 else "❌ FAIL\n") + out, reply_markup=backup_kb)
finally:
release_lock("backup")
pos = await enqueue(job)
await msg.answer(f"🕓 Backup queued (#{pos})", reply_markup=backup_kb)
async def cmd_last_snapshot(msg: Message):
await msg.answer("⏳ Loading last snapshot…", reply_markup=backup_kb)
async def worker():
rc, raw = await run_cmd(
["restic", "snapshots", "--json"],
use_restic_env=True,
timeout=20
)
if rc != 0:
await msg.answer(raw, reply_markup=backup_kb)
return
snaps = json.loads(raw)
if not snaps:
await msg.answer("📦 Snapshots: none", reply_markup=backup_kb)
return
snaps.sort(key=lambda s: s["time"], reverse=True)
s = snaps[0]
t = datetime.fromisoformat(s["time"].replace("Z", "+00:00"))
short_id = s["short_id"]
rc2, raw2 = await run_cmd(
["restic", "stats", short_id, "--json"],
use_restic_env=True,
timeout=20
)
if rc2 != 0:
await msg.answer(raw2, reply_markup=backup_kb)
return
stats = json.loads(raw2)
msg_text = (
"📦 **Last snapshot**\n\n"
f"🕒 {t:%Y-%m-%d %H:%M}\n"
f"🧉 ID: {short_id}\n"
f"📁 Files: {stats.get('total_file_count', 'n/a')}\n"
f"💽 Size: {stats.get('total_size', 0) / (1024**3):.2f} GiB\n"
)
await msg.answer(msg_text, reply_markup=backup_kb, parse_mode="Markdown")
asyncio.create_task(worker())
@dp.message(F.text == "📦 Status")
async def bs(msg: Message):
if is_admin_msg(msg):
await cmd_backup_status(msg)
@dp.message(F.text == "📊 Repo stats")
async def rs(msg: Message):
if is_admin_msg(msg):
await cmd_repo_stats(msg)
@dp.message(F.text == "📦 Last snapshot")
async def ls(msg: Message):
if is_admin_msg(msg):
await cmd_last_snapshot(msg)
@dp.message(F.text == "▶️ Run backup")
async def br(msg: Message):
if is_admin_msg(msg):
await cmd_backup_now(msg)
@dp.message(F.text == "🧯 Restore help")
async def rh(msg: Message):
if is_admin_msg(msg):
await msg.answer(restore_help(), reply_markup=backup_kb)