diff --git a/README.md b/README.md index 9b537c5..a151e0c 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,11 @@ sudo ./scripts/install.sh ``` - 超过60分钟的0字节 raw,如果未被进程占用,也会按上述三件套删除; +- 对`_raw.mp4.partN.mp4`录制残留执行整组安全检查: + - 同一base的所有part都必须超过60分钟、全部为0字节且均未被进程打开; + - 如果正式`_raw.mp4`已经存在,只删除符合条件的残留part,保留正常三件套; + - 如果正式raw不存在,删除全部残留part及对应的annotated视频和XML; + - 任一part仍较新、非0字节或正在使用时,整组跳过; - 空闲低于3GiB时暂停整个 `smart_camera` 进程(持续检测、录像和识别一起暂停),Producer继续上传,并输出 CRITICAL 告警; - 暂停期间每2分钟检查一次; - 清理使空闲恢复到5GiB后自动恢复 `smart_camera`;程序重新持续检测,达到鸟类确认门控后再录像和识别,并停止2分钟快速检查; diff --git a/bin/cleanup-smartcam-data b/bin/cleanup-smartcam-data index a5026bb..892e592 100755 --- a/bin/cleanup-smartcam-data +++ b/bin/cleanup-smartcam-data @@ -224,9 +224,97 @@ delete_stale_zero_bundles() { fi } +delete_stale_zero_part_bundles() { + local part filename base final_raw sibling path size + local safe part_count deleted_bundles=0 deleted_files=0 deleted_bytes=0 + declare -A checked_bases=() + + while IFS= read -r -d '' part; do + [[ -f "$part" ]] || continue + filename="$(basename "$part")" + base="${filename%%_raw.mp4.part*}" + [[ -n "$base" && -z "${checked_bases[$base]:-}" ]] || continue + checked_bases["$base"]=1 + final_raw="$DATA_ROOT/videos_raw/${base}_raw.mp4" + + # If a valid final raw exists, these are harmless recorder leftovers. + # Delete only old, zero-byte, unopened part files; preserve the bundle. + if [[ -f "$final_raw" ]]; then + while IFS= read -r -d '' sibling; do + find "$sibling" -mmin "+${MIN_FILE_AGE_MINUTES}" -print -quit 2>/dev/null | + grep -q . || continue + if command -v fuser >/dev/null 2>&1 && fuser -s "$sibling" 2>/dev/null; then + log WARNING "stale zero-byte part is still open; skipped: ${sibling}" + continue + fi + if rm -f -- "$sibling"; then + deleted_files=$((deleted_files + 1)) + log INFO "deleted stale zero-byte recorder part: ${sibling}" + fi + done < <(find "$DATA_ROOT/videos_raw" -maxdepth 1 -type f \ + -name "${base}_raw.mp4.part*.mp4" -size 0 -print0 2>/dev/null) + continue + fi + + # Without a final raw, only remove the incomplete bundle when every part + # is old, zero bytes, and not held open by any process. + safe=1 + part_count=0 + while IFS= read -r -d '' sibling; do + part_count=$((part_count + 1)) + [[ "$(stat -c %s "$sibling" 2>/dev/null || printf 1)" -eq 0 ]] || { + safe=0 + break + } + find "$sibling" -mmin "+${MIN_FILE_AGE_MINUTES}" -print -quit 2>/dev/null | + grep -q . || { + safe=0 + break + } + if command -v fuser >/dev/null 2>&1 && fuser -s "$sibling" 2>/dev/null; then + log WARNING "incomplete part bundle is still open; skipped: ${sibling}" + safe=0 + break + fi + done < <(find "$DATA_ROOT/videos_raw" -maxdepth 1 -type f \ + -name "${base}_raw.mp4.part*.mp4" -print0 2>/dev/null) + + [[ "$safe" -eq 1 && "$part_count" -gt 0 ]] || continue + + while IFS= read -r -d '' sibling; do + size="$(stat -c %s "$sibling" 2>/dev/null || printf 0)" + if rm -f -- "$sibling"; then + deleted_files=$((deleted_files + 1)) + deleted_bytes=$((deleted_bytes + size)) + log INFO "deleted stale incomplete bundle part: ${sibling}" + fi + done < <(find "$DATA_ROOT/videos_raw" -maxdepth 1 -type f \ + -name "${base}_raw.mp4.part*.mp4" -print0 2>/dev/null) + + for path in \ + "$DATA_ROOT/videos/${base}_annotated.mp4" \ + "$DATA_ROOT/annotations/${base}.xml"; do + [[ -f "$path" ]] || continue + size="$(stat -c %s "$path" 2>/dev/null || printf 0)" + if rm -f -- "$path"; then + deleted_files=$((deleted_files + 1)) + deleted_bytes=$((deleted_bytes + size)) + log INFO "deleted stale incomplete bundle member: ${path}" + fi + done + deleted_bundles=$((deleted_bundles + 1)) + done < <(find "$DATA_ROOT/videos_raw" -maxdepth 1 -type f \ + -name '*_raw.mp4.part*.mp4' -size 0 -print0 2>/dev/null) + + if [[ "$deleted_bundles" -gt 0 || "$deleted_files" -gt 0 ]]; then + log INFO "stale part cleanup result: bundles=${deleted_bundles} files=${deleted_files} bytes=${deleted_bytes}" + fi +} + main() { local free_bytes processed bundles delete_stale_zero_bundles + delete_stale_zero_part_bundles free_bytes="$(available_bytes)" # Protect the disk before cleanup work: once free space is below 3 GiB,