feat(fam-edge): 新增 DiskGuard 磁盘空间守护,剩余空间不足自动清理旧素材
背景:Oracle 磁盘曾经被写满(gdrive_videos 持续下载新素材、旧文件迟迟没 清理),触发 rclone 的一个安全机制——同步遇到 IO 错误就整体拒绝执行删除, 形成"越满越删不掉,越删不掉越满"的死循环,最终连新视频都下载不了。这次 排查+手动清理已经解决了当次故障,但需要一道独立于 rclone 同步之外的兜底, 防止再次悄悄写满没人发现。 - oracle_db.py 新增 get_oldest_purgeable_material():只挑最旧的、已完成 分割阶段(status='done')的整段素材(非 motion_ 前缀),绝不碰运动片段 (事件时间轴/人物头像依赖它)和还在处理中的素材 - disk_guard.py 新增 DiskGuard 后台线程:5 分钟检查一次,剩余空间 <10GB 触发清理,删到 15GB 水位为止(留缓冲避免刚清完又立刻触发),复用已有的 delete_video() 完成实际删除 - app.py 启动这个后台服务;/api/oracle/activity 新增 disk 字段(实时剩余 空间 + 最近一次清理动作),供服务状态页展示 - 新增 9 个单元测试,全部通过(139/139) 已部署 Oracle 验证:DiskGuard 正常启动,配置生效。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,45 @@ def test_delete_video_nonexistent_returns_none(tmp_path):
|
||||
assert db.delete_video(99999) is None
|
||||
|
||||
|
||||
def test_get_oldest_purgeable_material_none_when_empty(tmp_path):
|
||||
db = _db(tmp_path)
|
||||
assert db.get_oldest_purgeable_material() is None
|
||||
|
||||
|
||||
def test_get_oldest_purgeable_material_ignores_motion_clips(tmp_path):
|
||||
"""核心诉求: 运动片段(motion_ 前缀)是独立的分析产物,事件时间轴/人物
|
||||
头像都依赖它,磁盘清理绝不能碰它,只能清理原始整段素材。"""
|
||||
db = _db(tmp_path)
|
||||
vid = db.ensure_video("motion_1_1000.mp4", "/tmp/motion_1_1000.mp4",
|
||||
event_start_time="2026-08-22 10:00:00")
|
||||
db.mark_video_processed(vid, "摘要", [], [], "gemini")
|
||||
assert db.get_oldest_purgeable_material() is None
|
||||
|
||||
|
||||
def test_get_oldest_purgeable_material_ignores_non_done_status(tmp_path):
|
||||
"""核心诉求: 还在 pending/processing 的素材不能被清理,避免删掉还没
|
||||
来得及处理的数据。"""
|
||||
db = _db(tmp_path)
|
||||
db.ensure_video("Generic_ONVIF-001-20260815-000000.mp4",
|
||||
"/tmp/Generic_ONVIF-001-20260815-000000.mp4")
|
||||
assert db.get_oldest_purgeable_material() is None
|
||||
|
||||
|
||||
def test_get_oldest_purgeable_material_returns_oldest_done_material(tmp_path):
|
||||
db = _db(tmp_path)
|
||||
vid1 = db.ensure_video("Generic_ONVIF-001-20260815-000000.mp4",
|
||||
"/tmp/Generic_ONVIF-001-20260815-000000.mp4")
|
||||
db.mark_video_processed(vid1, "(整段素材已分割 0 段运动片段)", [], [], 'motion_segment')
|
||||
vid2 = db.ensure_video("Generic_ONVIF-001-20260816-000000.mp4",
|
||||
"/tmp/Generic_ONVIF-001-20260816-000000.mp4")
|
||||
db.mark_video_processed(vid2, "(整段素材已分割 0 段运动片段)", [], [], 'motion_segment')
|
||||
|
||||
candidate = db.get_oldest_purgeable_material()
|
||||
|
||||
assert candidate["id"] == vid1
|
||||
assert candidate["local_path"] == "/tmp/Generic_ONVIF-001-20260815-000000.mp4"
|
||||
|
||||
|
||||
def test_delete_video_does_not_touch_ss_motion_events(tmp_path):
|
||||
"""核心诉求: ss_motion_events 是运动侦测源事件,跟切出来的视频片段生命周期
|
||||
独立,删视频不该连带删掉源事件(否则分割逻辑的幂等判断会被破坏)。"""
|
||||
|
||||
Reference in New Issue
Block a user