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.backup import backup_badge, last_backup, 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): if not acquire_lock("backup"): await msg.answer("⚠️ Backup уже выполняется", reply_markup=backup_kb) return await msg.answer("▶️ Backup запущен", reply_markup=backup_kb) async def worker(): 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") asyncio.create_task(worker()) async def cmd_last_backup(msg: Message): try: text = await asyncio.to_thread(last_backup) except Exception as e: await msg.answer(f"❌ Last backup failed: {type(e).__name__}: {e}", reply_markup=backup_kb) return await msg.answer(text, reply_markup=backup_kb) @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 backup") async def lb(msg: Message): if is_admin_msg(msg): await cmd_last_backup(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)