From a98292604aacae1798ad56c8c5e9cc8c546c1904 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Feb 2026 03:54:51 +0300 Subject: [PATCH] Harden backup JSON parsing and fix queue display --- handlers/backup.py | 47 ++++++++++++++++++++++++++++++++++++++++------ services/queue.py | 22 +++++++++++----------- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/handlers/backup.py b/handlers/backup.py index 2e9aee4..994aca4 100644 --- a/handlers/backup.py +++ b/handlers/backup.py @@ -30,6 +30,17 @@ async def _unit_status(unit: str, props: list[str]) -> dict[str, str]: 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): services = [ ("backup-auto", "backup-auto.timer"), @@ -78,7 +89,11 @@ async def cmd_repo_stats(msg: Message): await msg.answer(raw1, reply_markup=backup_kb) 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 --- rc2, raw2 = await run_cmd( @@ -90,7 +105,11 @@ async def cmd_repo_stats(msg: Message): await msg.answer(raw2, reply_markup=backup_kb) 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 --- rc3, raw_snaps = await run_cmd( @@ -98,7 +117,14 @@ async def cmd_repo_stats(msg: Message): use_restic_env=True, 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 = ( "๐Ÿ“ฆ **Repository stats**\n\n" @@ -124,7 +150,10 @@ async def cmd_backup_status(msg: Message): await msg.answer(raw, reply_markup=backup_kb) 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: await msg.answer("๐Ÿ“ฆ Snapshots: none", reply_markup=backup_kb) return @@ -193,7 +222,10 @@ async def cmd_last_snapshot(msg: Message): await msg.answer(raw, reply_markup=backup_kb) 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: await msg.answer("๐Ÿ“ฆ Snapshots: none", reply_markup=backup_kb) return @@ -212,7 +244,10 @@ async def cmd_last_snapshot(msg: Message): await msg.answer(raw2, reply_markup=backup_kb) 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 = ( "๐Ÿ“ฆ **Last snapshot**\n\n" diff --git a/services/queue.py b/services/queue.py index c6df127..eb89c73 100644 --- a/services/queue.py +++ b/services/queue.py @@ -41,30 +41,30 @@ async def worker(): def format_status() -> str: pending = list(_pending) - lines = ["?? Queue"] - lines.append(f"?? Running: {_current_label or 'idle'}") - lines.append(f"? Pending: {len(pending)}") + lines = ["๐Ÿงพ Queue"] + lines.append(f"๐Ÿ”„ Running: {_current_label or 'idle'}") + lines.append(f"โณ Pending: {len(pending)}") if pending: preview = ", ".join([p[0] for p in pending[:5]]) - lines.append(f"?? Next: {preview}") - return "".join(lines) + lines.append(f"โžก๏ธ Next: {preview}") + return "\n".join(lines) def format_details(limit: int = 10) -> str: now = time.time() - lines = ["?? Queue details"] + lines = ["๐Ÿงพ Queue details"] if _current_label: 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" - lines.append(f"?? Running: {_current_label} ({runtime})") + lines.append(f"๐Ÿ”„ Running: {_current_label} ({runtime})") else: - lines.append("?? Running: idle") + lines.append("๐Ÿ”„ Running: idle") pending = list(_pending) - lines.append(f"? Pending: {len(pending)}") + lines.append(f"โณ Pending: {len(pending)}") if pending: - lines.append("?? Position | Label | Wait") + lines.append("๐Ÿ”ข Position | Label | Wait") for i, (label, enqueued_at) in enumerate(pending[:limit], start=1): wait = int(now - enqueued_at) lines.append(f"{i:>3} | {label} | {wait}s") - return "".join(lines) + return "\n".join(lines)