Harden backup JSON parsing and fix queue display

This commit is contained in:
2026-02-08 03:54:51 +03:00
parent 97524b92a2
commit a98292604a
2 changed files with 52 additions and 17 deletions

View File

@@ -30,6 +30,17 @@ async def _unit_status(unit: str, props: list[str]) -> dict[str, str]:
return _parse_systemctl_kv(out) return _parse_systemctl_kv(out)
def _load_json(raw: str, label: str) -> tuple[bool, object | None, str]:
if not raw or not raw.strip():
return False, None, f"? {label} returned empty output"
try:
return True, json.loads(raw), ""
except json.JSONDecodeError:
preview = raw.strip().splitlines()
head = preview[0] if preview else "invalid output"
return False, None, f"? {label} invalid JSON: {head}"
async def send_backup_jobs_status(msg: Message): async def send_backup_jobs_status(msg: Message):
services = [ services = [
("backup-auto", "backup-auto.timer"), ("backup-auto", "backup-auto.timer"),
@@ -78,7 +89,11 @@ async def cmd_repo_stats(msg: Message):
await msg.answer(raw1, reply_markup=backup_kb) await msg.answer(raw1, reply_markup=backup_kb)
return return
restore = json.loads(raw1) ok, restore, err = _load_json(raw1, "restic stats")
if not ok:
await msg.answer(err, reply_markup=backup_kb)
return
# --- raw-data stats --- # --- raw-data stats ---
rc2, raw2 = await run_cmd( rc2, raw2 = await run_cmd(
@@ -90,7 +105,11 @@ async def cmd_repo_stats(msg: Message):
await msg.answer(raw2, reply_markup=backup_kb) await msg.answer(raw2, reply_markup=backup_kb)
return return
raw = json.loads(raw2) ok, raw, err = _load_json(raw2, "restic stats raw-data")
if not ok:
await msg.answer(err, reply_markup=backup_kb)
return
# --- snapshots count --- # --- snapshots count ---
rc3, raw_snaps = await run_cmd( rc3, raw_snaps = await run_cmd(
@@ -98,7 +117,14 @@ async def cmd_repo_stats(msg: Message):
use_restic_env=True, use_restic_env=True,
timeout=20 timeout=20
) )
snaps = len(json.loads(raw_snaps)) if rc3 == 0 else "n/a" if rc3 != 0:
snaps = "n/a"
else:
ok, snap_data, err = _load_json(raw_snaps, "restic snapshots")
if ok and isinstance(snap_data, list):
snaps = len(snap_data)
else:
snaps = "n/a"
msg_text = ( msg_text = (
"📦 **Repository stats**\n\n" "📦 **Repository stats**\n\n"
@@ -124,7 +150,10 @@ async def cmd_backup_status(msg: Message):
await msg.answer(raw, reply_markup=backup_kb) await msg.answer(raw, reply_markup=backup_kb)
return return
snaps = json.loads(raw) ok, snaps, err = _load_json(raw, "restic snapshots")
if not ok or not isinstance(snaps, list):
await msg.answer(err, reply_markup=backup_kb)
return
if not snaps: if not snaps:
await msg.answer("📦 Snapshots: none", reply_markup=backup_kb) await msg.answer("📦 Snapshots: none", reply_markup=backup_kb)
return return
@@ -193,7 +222,10 @@ async def cmd_last_snapshot(msg: Message):
await msg.answer(raw, reply_markup=backup_kb) await msg.answer(raw, reply_markup=backup_kb)
return return
snaps = json.loads(raw) ok, snaps, err = _load_json(raw, "restic snapshots")
if not ok or not isinstance(snaps, list):
await msg.answer(err, reply_markup=backup_kb)
return
if not snaps: if not snaps:
await msg.answer("📦 Snapshots: none", reply_markup=backup_kb) await msg.answer("📦 Snapshots: none", reply_markup=backup_kb)
return return
@@ -212,7 +244,10 @@ async def cmd_last_snapshot(msg: Message):
await msg.answer(raw2, reply_markup=backup_kb) await msg.answer(raw2, reply_markup=backup_kb)
return return
stats = json.loads(raw2) ok, stats, err = _load_json(raw2, f"restic stats {short_id}")
if not ok or not isinstance(stats, dict):
await msg.answer(err, reply_markup=backup_kb)
return
msg_text = ( msg_text = (
"📦 **Last snapshot**\n\n" "📦 **Last snapshot**\n\n"

View File

@@ -41,30 +41,30 @@ async def worker():
def format_status() -> str: def format_status() -> str:
pending = list(_pending) pending = list(_pending)
lines = ["?? Queue"] lines = ["🧾 Queue"]
lines.append(f"?? Running: {_current_label or 'idle'}") lines.append(f"🔄 Running: {_current_label or 'idle'}")
lines.append(f"? Pending: {len(pending)}") lines.append(f" Pending: {len(pending)}")
if pending: if pending:
preview = ", ".join([p[0] for p in pending[:5]]) preview = ", ".join([p[0] for p in pending[:5]])
lines.append(f"?? Next: {preview}") lines.append(f"➡️ Next: {preview}")
return "".join(lines) return "\n".join(lines)
def format_details(limit: int = 10) -> str: def format_details(limit: int = 10) -> str:
now = time.time() now = time.time()
lines = ["?? Queue details"] lines = ["🧾 Queue details"]
if _current_label: if _current_label:
started_at = _current_meta.get("started_at") if _current_meta else None started_at = _current_meta.get("started_at") if _current_meta else None
runtime = f"{int(now - started_at)}s" if started_at else "n/a" runtime = f"{int(now - started_at)}s" if started_at else "n/a"
lines.append(f"?? Running: {_current_label} ({runtime})") lines.append(f"🔄 Running: {_current_label} ({runtime})")
else: else:
lines.append("?? Running: idle") lines.append("🔄 Running: idle")
pending = list(_pending) pending = list(_pending)
lines.append(f"? Pending: {len(pending)}") lines.append(f" Pending: {len(pending)}")
if pending: if pending:
lines.append("?? Position | Label | Wait") lines.append("🔢 Position | Label | Wait")
for i, (label, enqueued_at) in enumerate(pending[:limit], start=1): for i, (label, enqueued_at) in enumerate(pending[:limit], start=1):
wait = int(now - enqueued_at) wait = int(now - enqueued_at)
lines.append(f"{i:>3} | {label} | {wait}s") lines.append(f"{i:>3} | {label} | {wait}s")
return "".join(lines) return "\n".join(lines)