feat: 军事科技每日资讯推送系统 - Docker部署 + 日志系统 + 数据目录重组
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""根据 config.json 渲染 systemd 单元模板。
|
||||
|
||||
被 scripts/install.sh 调用。本身不直接读环境,需要的运行时参数(user/group/uv 路径)
|
||||
通过 CLI 注入,便于 install.sh 处理 sudo 场景。
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def cron_to_oncalendar(expr: str) -> str:
|
||||
"""5 段 cron 表达式 → systemd OnCalendar 字符串。
|
||||
|
||||
本项目 push_cron 只用到 minute/hour,其他位必须为 `*`。
|
||||
遇到不支持的语法直接报错,避免静默生成错误的 timer。
|
||||
"""
|
||||
parts = expr.split()
|
||||
if len(parts) != 5:
|
||||
raise ValueError(f"无效 cron 表达式(必须 5 段): {expr}")
|
||||
minute, hour, dom, mon, dow = parts
|
||||
for label, val in [("day-of-month", dom), ("month", mon), ("day-of-week", dow)]:
|
||||
if val != "*":
|
||||
raise ValueError(f"暂不支持非 * 的 {label} 字段: {expr}")
|
||||
try:
|
||||
m, h = int(minute), int(hour)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"minute/hour 必须是整数: {expr}") from e
|
||||
if not (0 <= m <= 59 and 0 <= h <= 23):
|
||||
raise ValueError(f"minute/hour 越界: {expr}")
|
||||
return f"*-*-* {h:02d}:{m:02d}:00"
|
||||
|
||||
|
||||
def render(template_path: Path, variables: dict) -> str:
|
||||
text = template_path.read_text(encoding="utf-8")
|
||||
for key, val in variables.items():
|
||||
text = text.replace(f"{{{{{key}}}}}", str(val))
|
||||
return text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--project-dir", required=True)
|
||||
parser.add_argument("--user", required=True)
|
||||
parser.add_argument("--group", required=True)
|
||||
parser.add_argument("--uv-bin", required=True)
|
||||
parser.add_argument("--output-dir", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
project_dir = Path(args.project_dir).resolve()
|
||||
output_dir = Path(args.output_dir).resolve()
|
||||
template_dir = project_dir / "systemd"
|
||||
|
||||
config_path = project_dir / "config.json"
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
fetch_min = config["schedule"]["fetch_interval_minutes"]
|
||||
if not isinstance(fetch_min, int) or fetch_min <= 0:
|
||||
raise ValueError(f"fetch_interval_minutes 必须是正整数: {fetch_min}")
|
||||
|
||||
push_crons = config["schedule"]["push_cron"]
|
||||
if not push_crons:
|
||||
raise ValueError("config.schedule.push_cron 不能为空")
|
||||
push_lines = "\n".join(
|
||||
f"OnCalendar={cron_to_oncalendar(c)}" for c in push_crons
|
||||
)
|
||||
|
||||
log_retention = config.get("log", {}).get("retention_days", 7)
|
||||
if not isinstance(log_retention, int) or log_retention <= 0:
|
||||
raise ValueError(f"log.retention_days 必须是正整数: {log_retention}")
|
||||
|
||||
common = {
|
||||
"PROJECT_DIR": str(project_dir),
|
||||
"USER": args.user,
|
||||
"GROUP": args.group,
|
||||
"UV_BIN": args.uv_bin,
|
||||
}
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
renderings = [
|
||||
("dnews-fetch.service.tmpl", "dnews-fetch.service", {}),
|
||||
("dnews-fetch.timer.tmpl", "dnews-fetch.timer",
|
||||
{"FETCH_INTERVAL_MIN": fetch_min}),
|
||||
("dnews-push.service.tmpl", "dnews-push.service", {}),
|
||||
("dnews-push.timer.tmpl", "dnews-push.timer",
|
||||
{"PUSH_ONCALENDAR_LINES": push_lines}),
|
||||
("journald-dnews.conf.tmpl", "journald-dnews.conf",
|
||||
{"LOG_RETENTION_DAYS": log_retention}),
|
||||
]
|
||||
|
||||
print(f"📂 输出目录: {output_dir}")
|
||||
for tpl_name, out_name, extra in renderings:
|
||||
tpl_path = template_dir / tpl_name
|
||||
if not tpl_path.exists():
|
||||
print(f"❌ 模板不存在: {tpl_path}", file=sys.stderr)
|
||||
return 2
|
||||
rendered = render(tpl_path, {**common, **extra})
|
||||
(output_dir / out_name).write_text(rendered, encoding="utf-8")
|
||||
print(f" ✓ {out_name}")
|
||||
|
||||
print(f"\n生成参数:")
|
||||
print(f" fetch 间隔 → 每 {fetch_min} 分钟(间隔触发,从上次完成开始计时)")
|
||||
for c, line in zip(push_crons, push_lines.split("\n")):
|
||||
print(f" push '{c}' → {line}")
|
||||
print(f" 日志保留 → {log_retention} 天")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user