Beautify restic forget table in backup history
This commit is contained in:
@@ -74,6 +74,98 @@ def _tail(path: str, lines: int = 120) -> str:
|
|||||||
return "".join(data).strip() or "(empty)"
|
return "".join(data).strip() or "(empty)"
|
||||||
|
|
||||||
|
|
||||||
|
def _beautify_restic_forget(raw: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Parse restic forget output tables into a compact bullet list.
|
||||||
|
"""
|
||||||
|
if "Reasons" not in raw or "Paths" not in raw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lines = raw.splitlines()
|
||||||
|
headers = []
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
if line.startswith("ID") and "Reasons" in line and "Paths" in line:
|
||||||
|
headers.append(idx)
|
||||||
|
if not headers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_block(start_idx: int, end_idx: int) -> list[dict]:
|
||||||
|
header = lines[start_idx]
|
||||||
|
cols = ["ID", "Time", "Host", "Tags", "Reasons", "Paths", "Size"]
|
||||||
|
positions = []
|
||||||
|
for name in cols:
|
||||||
|
pos = header.find(name)
|
||||||
|
if pos == -1:
|
||||||
|
return []
|
||||||
|
positions.append(pos)
|
||||||
|
positions.append(len(header))
|
||||||
|
|
||||||
|
entries: list[dict] = []
|
||||||
|
current: dict | None = None
|
||||||
|
for line in lines[start_idx + 2 : end_idx]:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
segments = []
|
||||||
|
for i in range(len(cols)):
|
||||||
|
segments.append(line[positions[i] : positions[i + 1]].strip())
|
||||||
|
row = dict(zip(cols, segments))
|
||||||
|
if row["ID"]:
|
||||||
|
current = {
|
||||||
|
"id": row["ID"],
|
||||||
|
"time": row["Time"],
|
||||||
|
"host": row["Host"],
|
||||||
|
"size": row["Size"],
|
||||||
|
"tags": row["Tags"],
|
||||||
|
"reasons": [],
|
||||||
|
"paths": [],
|
||||||
|
}
|
||||||
|
if row["Reasons"]:
|
||||||
|
current["reasons"].append(row["Reasons"])
|
||||||
|
if row["Paths"]:
|
||||||
|
current["paths"].append(row["Paths"])
|
||||||
|
entries.append(current)
|
||||||
|
elif current:
|
||||||
|
if row["Reasons"]:
|
||||||
|
current["reasons"].append(row["Reasons"])
|
||||||
|
if row["Paths"]:
|
||||||
|
current["paths"].append(row["Paths"])
|
||||||
|
return entries
|
||||||
|
|
||||||
|
blocks = []
|
||||||
|
for i, start in enumerate(headers):
|
||||||
|
end = headers[i + 1] if i + 1 < len(headers) else len(lines)
|
||||||
|
entries = parse_block(start, end)
|
||||||
|
if not entries:
|
||||||
|
continue
|
||||||
|
label = "Plan"
|
||||||
|
prev_line = lines[start - 1].lower() if start - 1 >= 0 else ""
|
||||||
|
prev2 = lines[start - 2].lower() if start - 2 >= 0 else ""
|
||||||
|
if "keep" in prev_line:
|
||||||
|
label = prev_line.strip()
|
||||||
|
elif "keep" in prev2:
|
||||||
|
label = prev2.strip()
|
||||||
|
elif "snapshots" in prev_line:
|
||||||
|
label = prev_line.strip()
|
||||||
|
blocks.append((label, entries))
|
||||||
|
|
||||||
|
if not blocks:
|
||||||
|
return None
|
||||||
|
|
||||||
|
out_lines = []
|
||||||
|
for label, entries in blocks:
|
||||||
|
out_lines.append(f"📦 {label}")
|
||||||
|
for e in entries:
|
||||||
|
head = f"🧉 {e['id']} | {e['time']} | {e['host']} | {e['size'] or 'n/a'}"
|
||||||
|
out_lines.append(head)
|
||||||
|
if e["reasons"]:
|
||||||
|
out_lines.append(" 📌 " + "; ".join(e["reasons"]))
|
||||||
|
if e["paths"]:
|
||||||
|
for p in e["paths"]:
|
||||||
|
out_lines.append(f" • {p}")
|
||||||
|
out_lines.append("")
|
||||||
|
return "\n".join(out_lines).rstrip()
|
||||||
|
|
||||||
|
|
||||||
def _load_json(raw: str, label: str) -> tuple[bool, object | None, str]:
|
def _load_json(raw: str, label: str) -> tuple[bool, object | None, str]:
|
||||||
if not raw or not raw.strip():
|
if not raw or not raw.strip():
|
||||||
return False, None, f"? {label} returned empty output"
|
return False, None, f"? {label} returned empty output"
|
||||||
@@ -436,6 +528,7 @@ async def backup_history(msg: Message):
|
|||||||
if content.startswith("⚠️"):
|
if content.startswith("⚠️"):
|
||||||
await msg.answer(content, reply_markup=backup_kb)
|
await msg.answer(content, reply_markup=backup_kb)
|
||||||
return
|
return
|
||||||
|
pretty = _beautify_restic_forget(content)
|
||||||
trimmed = False
|
trimmed = False
|
||||||
max_len = 3500
|
max_len = 3500
|
||||||
if len(content) > max_len:
|
if len(content) > max_len:
|
||||||
@@ -444,6 +537,9 @@ async def backup_history(msg: Message):
|
|||||||
header = "📜 Backup history (tail)"
|
header = "📜 Backup history (tail)"
|
||||||
if trimmed:
|
if trimmed:
|
||||||
header += " (trimmed)"
|
header += " (trimmed)"
|
||||||
|
if pretty:
|
||||||
|
await msg.answer(f"{header}\n`{log_path}`\n\n{pretty}", reply_markup=backup_kb)
|
||||||
|
else:
|
||||||
await msg.answer(
|
await msg.answer(
|
||||||
f"{header}\n`{log_path}`\n```\n{content}\n```",
|
f"{header}\n`{log_path}`\n```\n{content}\n```",
|
||||||
reply_markup=backup_kb,
|
reply_markup=backup_kb,
|
||||||
|
|||||||
Reference in New Issue
Block a user