feat(video-analysis): 群策略 v3 / 群文件投递通道 / Web 管理页
- policy.py:per-group 正交策略(自动解析 / 自动策略 / 禁用策略 / 存储 A·B·C /
公网 / 链接 / 群文件 + 平台限定),list.json v1/v2 → v3 自动迁移,
写入统一走 PolicyStore(加锁 + .tmp 原子替换 + 字段归一)
- 群文件并行通道 group_file.py:打包 zip(可选 pyzipper AES-256)后优先走 S3 预签名、
本地直传兜底;设了密码但 pyzipper 不可用就放弃上传,不退化成明文
- list_proc.py 收敛到「视频策略」统一入口,权限判定改走 policy
- Web 管理页 /hub/video_analysis(群策略 + 链接解析面板)与 services/web_jobs.py
(只复用纯函数层,Web 上下文不发消息;内存任务表 + 并发闸门 + 超时)
- 媒体命名统一到 utils.py({作者}_{作者id}/{作品名}[_短码]),cleanup 回收空目录
- 测试:policy / 命名 / 群文件 / web_jobs 四组
顺带 pyproject 的 pytest 加 testpaths=tests(避免收进 debug/ 下的调试脚本)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,9 @@ hexi/data/temp 是下载媒体中转区(见 utils.get_temp_root),发送成功后
|
||||
- /清理temp [天数] 清理 temp 下超过 N 天(默认取配置)未修改的文件
|
||||
- /temp统计 查看 temp 目录占用情况
|
||||
|
||||
目录结构始终保留,占用中的文件自动跳过。
|
||||
媒体按「作者目录/作品目录」分层落盘,文件删完后这些空目录会一并收掉
|
||||
(只删同为过期、且确实为空的目录 —— 正在落盘的目录 mtime 很新,不会误删)。
|
||||
占用中的文件自动跳过。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -71,11 +73,39 @@ def _walk_files(root: Path) -> list[Path]:
|
||||
return files
|
||||
|
||||
|
||||
def _prune_empty_dirs(root: Path, max_age: float) -> int:
|
||||
"""自底向上收掉空目录(作者的层与作品的层都算),返回删除的目录数。
|
||||
|
||||
只删「本身就是空」且 mtime 已过期的目录:正在落盘的目录刚建出来、
|
||||
mtime 很新,不会被误删;刚删完文件的目录 mtime 会被刷新,留到下一轮。
|
||||
root 自身不在 rglob 结果里,不会被删。
|
||||
"""
|
||||
removed = 0
|
||||
# 目录另有 60s 下限:清理temp 0 时不能把"刚建出来、还没写第一个文件"的
|
||||
# 目录(下载落盘点先 mkdir 再 open)删掉
|
||||
dir_age = max(max_age, 60)
|
||||
dirs = [p for p in root.rglob("*") if p.is_dir()]
|
||||
# 深的先处理:子目录删掉后父目录才可能变空,同一轮里能被顺带收掉
|
||||
for path in sorted(dirs, key=lambda p: len(p.parts), reverse=True):
|
||||
try:
|
||||
if any(path.iterdir()):
|
||||
continue
|
||||
if not _file_is_stale(path, dir_age):
|
||||
continue
|
||||
path.rmdir()
|
||||
removed += 1
|
||||
logger.info(f"temp 清理: 删除空目录 {path}")
|
||||
except OSError:
|
||||
# 被占用 / 刚被别的进程删掉 → 留待下轮
|
||||
continue
|
||||
return removed
|
||||
|
||||
|
||||
def clean_temp_files(sub: str = "", days: int | None = None) -> tuple[int, int]:
|
||||
"""清理 temp[/sub] 下超过期限的文件。
|
||||
"""清理 temp[/sub] 下超过期限的文件与随之空掉的目录。
|
||||
|
||||
Returns:
|
||||
(removed, total) — 删除数、统计到的文件总数
|
||||
(removed, total) — 删除的文件数、统计到的文件总数
|
||||
"""
|
||||
root = get_temp_root(sub)
|
||||
if not root.is_dir():
|
||||
@@ -94,17 +124,22 @@ def clean_temp_files(sub: str = "", days: int | None = None) -> tuple[int, int]:
|
||||
except OSError as e:
|
||||
# 文件被占用(如发送中)等场景,留待下轮
|
||||
logger.warning(f"temp 清理: 跳过 {path} ({e})")
|
||||
|
||||
dirs = _prune_empty_dirs(root, max_age)
|
||||
if dirs:
|
||||
logger.info(f"temp 清理: 同时收掉 {dirs} 个空目录")
|
||||
return removed, len(files)
|
||||
|
||||
|
||||
def temp_stats(sub: str = "") -> dict:
|
||||
"""统计 temp[/sub] 目录:文件数、总大小(字节)"""
|
||||
"""统计 temp[/sub] 目录:文件数、总大小(字节)、目录数(含作者/作品层)"""
|
||||
root = get_temp_root(sub)
|
||||
if not root.is_dir():
|
||||
return {"files": 0, "bytes": 0}
|
||||
return {"files": 0, "bytes": 0, "dirs": 0}
|
||||
files = _walk_files(root)
|
||||
total_bytes = sum(p.stat().st_size for p in files if p.exists())
|
||||
return {"files": len(files), "bytes": total_bytes}
|
||||
dirs = sum(1 for p in root.rglob("*") if p.is_dir())
|
||||
return {"files": len(files), "bytes": total_bytes, "dirs": dirs}
|
||||
|
||||
|
||||
# ── 手动清理命令(manual 模式,auto 模式下也可用) ──────────────
|
||||
@@ -141,11 +176,18 @@ async def _handle_clean(bot: Bot, event: MessageEvent):
|
||||
async def _handle_stats(event: MessageEvent):
|
||||
st = await _run_stats()
|
||||
if st["files"] == 0:
|
||||
await UniMessage.text("temp 目录目前是空的。").send()
|
||||
if st.get("dirs"):
|
||||
await UniMessage.text(
|
||||
f"temp 目录下没有文件了,还剩 {st['dirs']} 个空目录"
|
||||
f"(下次清理/清理temp 会一并收掉)。"
|
||||
).send()
|
||||
else:
|
||||
await UniMessage.text("temp 目录目前是空的。").send()
|
||||
else:
|
||||
size_mb = st["bytes"] / 1024 / 1024
|
||||
await UniMessage.text(
|
||||
f"temp 目录:共 {st['files']} 个文件,占用 {size_mb:.1f} MB。"
|
||||
f"temp 目录:共 {st['files']} 个文件,占用 {size_mb:.1f} MB,"
|
||||
f"{st.get('dirs', 0)} 个目录。"
|
||||
).send()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user