mirror of
https://github.com/CodeSeeed/sbackup.git
synced 2026-10-03 18:32:58 +08:00
feat: HTML报告 — report --format html
This commit is contained in:
1 parent
db761d3537
commit
2c7ff07c12
4 files changed
+119
-6
No files matched your search
@@ -2023,3 +2023,106 @@ class BackupManager:
|
||||
print(t("cmd.report.saved", path=output_file))
|
||||
|
||||
return report
|
||||
|
||||
def export_html_report(self, output_file: str = "") -> str:
|
||||
"""生成 HTML 格式备份报告
|
||||
:param output_file: 输出文件路径,空字符串则返回 HTML 文本
|
||||
:return: HTML 报告文本
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
history = self.get_history()
|
||||
strategies = {
|
||||
k: v
|
||||
for k, v in self.data.items()
|
||||
if k not in (_HISTORY_KEY, "_file_meta", "_chunk_meta")
|
||||
}
|
||||
|
||||
# 计算统计
|
||||
total_size = sum(h.get("size_mb", 0) for h in history)
|
||||
total_files = sum(h.get("files_count", 0) for h in history)
|
||||
|
||||
# 策略行
|
||||
strategy_rows = ""
|
||||
for source, raw in strategies.items():
|
||||
entry = BackupEntry.from_list(raw)
|
||||
fmt = entry.compression_format or t("table.cell.default")
|
||||
skip_patterns = ", ".join(entry.skip_patterns) if entry.skip_patterns else "-"
|
||||
strategy_rows += f"""<tr>
|
||||
<td>{source}</td>
|
||||
<td>{entry.target}</td>
|
||||
<td>{fmt}</td>
|
||||
<td>{skip_patterns}</td>
|
||||
</tr>\n"""
|
||||
|
||||
# 历史行(最近 50 条)
|
||||
rows_html = ""
|
||||
for h in reversed(history[-50:]):
|
||||
tag = h.get("tag", "")
|
||||
tag_html = f'<span class="tag">{tag}</span>' if tag else ""
|
||||
ratio = h.get("ratio", 0)
|
||||
ratio_str = f"{ratio:.1f}%" if isinstance(ratio, (int, float)) else "-"
|
||||
rows_html += f"""<tr>
|
||||
<td>{h.get("time", "")}</td>
|
||||
<td>{h.get("source", "")}</td>
|
||||
<td class="num">{h.get("size_mb", 0):.2f}</td>
|
||||
<td class="num">{h.get("files_count", 0)}</td>
|
||||
<td class="num">{ratio_str}</td>
|
||||
<td>{tag_html}</td>
|
||||
</tr>\n"""
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>sbackup {t("cmd.report.title", default="备份报告")}</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; color: #333; }}
|
||||
h1 {{ color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }}
|
||||
h2 {{ color: #2c3e50; margin-top: 30px; }}
|
||||
.stats {{ display: flex; gap: 20px; flex-wrap: wrap; margin: 20px 0; }}
|
||||
.stat-box {{ background: #fff; border-radius: 8px; padding: 20px; flex: 1; min-width: 150px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; }}
|
||||
.stat-box .num {{ font-size: 28px; font-weight: bold; color: #3498db; }}
|
||||
.stat-box .label {{ font-size: 13px; color: #7f8c8d; margin-top: 5px; }}
|
||||
table {{ width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
|
||||
th {{ background: #2c3e50; color: #fff; padding: 12px; text-align: left; }}
|
||||
td {{ padding: 10px 12px; border-bottom: 1px solid #ecf0f1; }}
|
||||
tr:nth-child(even) {{ background: #f8f9fa; }}
|
||||
tr:hover {{ background: #e8f4fd; }}
|
||||
.num {{ text-align: right; font-family: 'Consolas', monospace; }}
|
||||
.tag {{ display: inline-block; background: #3498db; color: #fff; padding: 2px 8px; border-radius: 12px; font-size: 12px; }}
|
||||
.footer {{ text-align: center; color: #95a5a6; margin-top: 30px; font-size: 13px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>sbackup {t("cmd.report.title", default="备份报告")}</h1>
|
||||
<p>{t("cmd.report.generated", default="生成时间")}: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
|
||||
<div class="stats">
|
||||
<div class="stat-box"><div class="num">{len(strategies)}</div><div class="label">{t("cmd.report.strategies", default="备份策略")}</div></div>
|
||||
<div class="stat-box"><div class="num">{len(history)}</div><div class="label">{t("cmd.report.history", default="历史记录")}</div></div>
|
||||
<div class="stat-box"><div class="num">{total_size:.1f}</div><div class="label">{t("cmd.report.total_size", default="总大小 (MB)")}</div></div>
|
||||
<div class="stat-box"><div class="num">{total_files}</div><div class="label">{t("cmd.report.total_files", default="总文件数")}</div></div>
|
||||
</div>
|
||||
<h2>{t("cmd.report.strategy_overview", default="策略概览")}</h2>
|
||||
<table>
|
||||
<tr><th>{t("cmd.report.source", default="源目录")}</th><th>{t("cmd.report.target", default="目标目录")}</th><th>{t("cmd.report.format", default="格式")}</th><th>{t("cmd.report.skip_patterns", default="忽略模式")}</th></tr>
|
||||
{strategy_rows}
|
||||
</table>
|
||||
<h2>{t("cmd.report.history_header", default="备份历史 (最近50条)")}</h2>
|
||||
<table>
|
||||
<tr><th>{t("cmd.report.time", default="时间")}</th><th>{t("cmd.report.source", default="源目录")}</th><th>{t("cmd.report.size", default="大小(MB)")}</th><th>{t("cmd.report.files", default="文件数")}</th><th>{t("cmd.report.ratio", default="压缩率")}</th><th>{t("cmd.report.tag", default="标签")}</th></tr>
|
||||
{rows_html}
|
||||
</table>
|
||||
<div class="footer">{t("cmd.report.generated_by", default="Generated by sbackup")}</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
if output_file:
|
||||
output_dir = os.path.dirname(output_file)
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
print(t("cmd.report.saved", path=output_file))
|
||||
|
||||
return html
|
||||
@@ -466,6 +466,10 @@ def get_parser() -> argparse.ArgumentParser:
|
||||
report_parser.add_argument(
|
||||
"-o", "--output", default="", help=t("cli.help.report.output")
|
||||
)
|
||||
report_parser.add_argument(
|
||||
"--format", choices=["text", "html"], default="text",
|
||||
help=t("cli.help.report.format"),
|
||||
)
|
||||
|
||||
search_parser = subparsers.add_parser("search", help=t("cli.help.search"))
|
||||
search_parser.add_argument("pattern", help=t("cli.help.search.pattern"))
|
||||
@@ -1039,6 +1043,10 @@ def _handle_remote(args, config, manager) -> int:
|
||||
|
||||
|
||||
def _handle_report(args, config, manager) -> int:
|
||||
if args.format == "html":
|
||||
output = args.output or ""
|
||||
manager.export_html_report(output)
|
||||
return 0
|
||||
report = manager.export_report(args.output)
|
||||
if not args.output:
|
||||
print(report)
|
||||
|
||||
@@ -363,8 +363,8 @@
|
||||
"err.webdav.not_found": "Remote file not found: {path}",
|
||||
"err.webdav.delete": "Failed to delete remote file {path}: {error}",
|
||||
"err.webdav.list": "Failed to list remote files {path}: {error}",
|
||||
"restore.no_match": "No files matching \"{pattern}\"",
|
||||
"cli.help.restore.select": "Selective restore: extract only files matching pattern (supports glob wildcards, e.g. \"*.py\" or \"src/main.py\")",
|
||||
"restore.no_match": 'No files matching "{pattern}"',
|
||||
"cli.help.restore.select": 'Selective restore: extract only files matching pattern (supports glob wildcards, e.g. "*.py" or "src/main.py")',
|
||||
"cli.help.schedule.type.schtasks": "Windows Task Scheduler",
|
||||
"cmd.schedule.schtasks.generated": "Windows Task Scheduler command generated",
|
||||
"cmd.hook.pre_running": "Running pre-backup hook: {command}",
|
||||
@@ -399,5 +399,6 @@
|
||||
"cmd.list.tags.header": "Backup Tags:",
|
||||
"cmd.list.tags.empty": "No tags found",
|
||||
"cli.help.dedup": "Enable content-based deduplication across backup strategies (SHA256)",
|
||||
"cmd.dedup.saved": "Deduplication: skipped {count} duplicate file(s), saved {saved:.2f} MB"
|
||||
"cmd.dedup.saved": "Deduplication: skipped {count} duplicate file(s), saved {saved:.2f} MB",
|
||||
"cli.help.report.format": "Report format: text (default) or html"
|
||||
}
|
||||
@@ -366,8 +366,8 @@
|
||||
"err.webdav.not_found": "远程文件不存在: {path}",
|
||||
"err.webdav.delete": "删除远程文件失败 {path}: {error}",
|
||||
"err.webdav.list": "列出远程文件失败 {path}: {error}",
|
||||
"restore.no_match": "没有匹配 \"{pattern}\" 的文件",
|
||||
"cli.help.restore.select": "选择性还原:仅提取匹配模式的文件(支持 glob 通配符,如 \"*.py\" 或 \"src/main.py\")",
|
||||
"restore.no_match": '没有匹配 "{pattern}" 的文件',
|
||||
"cli.help.restore.select": '选择性还原:仅提取匹配模式的文件(支持 glob 通配符,如 "*.py" 或 "src/main.py")',
|
||||
"cli.help.schedule.type.schtasks": "Windows 计划任务",
|
||||
"cmd.schedule.schtasks.generated": "Windows 计划任务命令已生成",
|
||||
"cmd.hook.pre_running": "正在执行前置钩子: {command}",
|
||||
@@ -402,5 +402,6 @@
|
||||
"cmd.list.tags.header": "备份标签:",
|
||||
"cmd.list.tags.empty": "暂无标签",
|
||||
"cli.help.dedup": "启用跨策略的基于内容哈希(SHA256)文件级去重",
|
||||
"cmd.dedup.saved": "去重: 跳过 {count} 个重复文件,节省 {saved:.2f} MB"
|
||||
"cmd.dedup.saved": "去重: 跳过 {count} 个重复文件,节省 {saved:.2f} MB",
|
||||
"cli.help.report.format": "报告格式: text(文本) 或 html(网页)"
|
||||
}
|
||||
Reference in new issue
Block a user