From 71c471128231a9218f585f9680688aa705962196 Mon Sep 17 00:00:00 2001 From: Ankle Date: Thu, 6 Aug 2026 18:33:25 +0800 Subject: [PATCH] gengxin --- README.md | 32 +- bin/cleanup-smartcam-data | 347 ++++++++++++++++----- scripts/install.sh | 13 +- scripts/uninstall.sh | 2 +- systemd/cleanup-smartcam-data-resume.timer | 3 +- tests/scan-processed-fixture.log | 1 + 6 files changed, 303 insertions(+), 95 deletions(-) create mode 100644 tests/scan-processed-fixture.log diff --git a/README.md b/README.md index a151e0c..12eba98 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ cleanup-smartcam-data/ ```text /var/lib/cleanup-smartcam-data/ /run/cleanup-smartcam-data.lock +/userdata/data/.recording-paused-low-disk ``` ## 环境要求 @@ -42,7 +43,8 @@ cleanup-smartcam-data/ - smart-cam 容器名:`smart-cam` - 数据目录:`/userdata/data` - 容器内成功记录:`/smart-cam/kafka_sender/processed_files.log` -- 采集进程命令包含:`build/bin/smart_camera` +- `smart_camera` 支持读取 `/smart-cam/data/.recording-paused-low-disk` + (与宿主机 `/userdata/data/.recording-paused-low-disk` 是同一挂载文件) - 宿主机使用 Docker 和 systemd ## 安装 @@ -83,14 +85,30 @@ sudo ./scripts/install.sh ``` - 超过60分钟的0字节 raw,如果未被进程占用,也会按上述三件套删除; +- 每次运行都会对`videos_raw/videos/annotations`按base进行完整性对账; + - 三个成员齐全才视为完整三件套; + - 缺少任意成员且所有现存成员均超过60分钟、未被进程打开时,删除该base的全部残留; + - 新录制、仍被进程打开或未满60分钟的文件不会被完整性对账误删; +- 已确认上传的正常清理在删除前会再次要求raw、annotated和XML三者齐全,缺一项整组跳过; +- scan 以一次扫描目录为原子单位;只有全部小图和`meta.json`的SFTP + 上传均完成、Kafka消息确认成功,并写入 + `/userdata/data/upload_processed_scans.log`后才允许清理; +- 已确认的scan还必须超过60分钟、`meta.json`状态为`completed`且目录内 + 没有文件被进程打开,随后才会整目录删除; - 对`_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分钟快速检查; +- 空闲低于3GiB时只创建“禁止录像”标记,立即结束正在生成的录像,并停止产生新的 + `videos`、`videos_raw`、`annotations`;持续检测、推理、RTSP/公网直播、scan和 + Producer上传均不暂停; +- 禁止录像期间每2分钟检查一次; +- 清理使空闲恢复到5GiB后删除标记并自动允许录像;持续检测一直运行,之后达到 + 15/25鸟类确认门控即可重新产生三件套,并停止2分钟快速检查; +- 从旧版本升级时,如果发现旧的 `capture-paused` 状态,程序会先向 + `smart_camera` 发送一次 `SIGCONT` 并删除旧状态,之后不再使用 + `SIGSTOP/SIGCONT` 控制整个进程; - 无法读取成功记录时不删除数据。 ## 验证 @@ -112,6 +130,8 @@ journalctl -u cleanup-smartcam-data.service -n 100 --no-pager ```bash docker exec smart-cam \ test -r /smart-cam/kafka_sender/processed_files.log + +test -r /userdata/data/upload_processed_scans.log ``` 确认 Docker 未重启: @@ -153,8 +173,8 @@ cd /home/zhangw/cleanup-smartcam-data sudo ./scripts/uninstall.sh ``` -卸载后检查采集是否处于暂停状态,再决定是否删除: +卸载不会自动删除当前的录像禁止标记;如需立即恢复录像,请确认空间充足后删除: ```text -/var/lib/cleanup-smartcam-data/ +/userdata/data/.recording-paused-low-disk ``` diff --git a/bin/cleanup-smartcam-data b/bin/cleanup-smartcam-data index 892e592..ff00d9f 100755 --- a/bin/cleanup-smartcam-data +++ b/bin/cleanup-smartcam-data @@ -1,16 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -TARGET_MOUNT="/userdata" -DATA_ROOT="/userdata/data" +TARGET_MOUNT="${TARGET_MOUNT:-/userdata}" +DATA_ROOT="${DATA_ROOT:-/userdata/data}" CONTAINER="smart-cam" PROCESSED_FILE="/smart-cam/kafka_sender/processed_files.log" -STATE_DIR="/var/lib/cleanup-smartcam-data" -PAUSED_STATE="${STATE_DIR}/capture-paused" -LOCK_FILE="/run/cleanup-smartcam-data.lock" +SCAN_PROCESSED_FILE="${DATA_ROOT}/upload_processed_scans.log" +STATE_DIR="${STATE_DIR:-/var/lib/cleanup-smartcam-data}" +RECORDING_PAUSE_MARKER="${RECORDING_PAUSE_MARKER:-${DATA_ROOT}/.recording-paused-low-disk}" +LEGACY_PAUSED_STATE="${STATE_DIR}/capture-paused" +LEGACY_OLD_PAUSED_STATE="/var/lib/userdata-video-cleanup/capture-paused" +LOCK_FILE="${LOCK_FILE:-/run/cleanup-smartcam-data.lock}" -# Delete confirmed bundles on every scheduled run. Pause capture and raise a -# critical alert below 3 GiB, and resume after recovery to 5 GiB. +# Delete confirmed bundles on every scheduled run. Disable only local video +# recording below 3 GiB; detection, RTSP/live output and upload stay running. +# Re-enable recording after recovery to 5 GiB. RESUME_TARGET_BYTES="${RESUME_TARGET_BYTES:-5368709120}" PAUSE_BELOW_BYTES="${PAUSE_BELOW_BYTES:-3221225472}" CRITICAL_BELOW_BYTES="${CRITICAL_BELOW_BYTES:-3221225472}" @@ -79,64 +83,43 @@ stop_fast_resume_checks() { systemctl --no-block stop cleanup-smartcam-data-resume.timer 2>/dev/null || true } -wait_for_safe_window() { - local newest size1 size2 - newest="$(find "$DATA_ROOT/videos_raw" -maxdepth 1 -type f -name '*.mp4' \ - -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-)" - [[ -n "$newest" && -f "$newest" ]] || return 0 - size1="$(stat -c %s "$newest" 2>/dev/null || printf 0)" - sleep "$SAFE_WINDOW_SECONDS" - [[ -f "$newest" ]] || return 0 - size2="$(stat -c %s "$newest" 2>/dev/null || printf 0)" - [[ "$size1" -eq "$size2" && "$size2" -gt 0 ]] +recover_legacy_capture_pause() { + local state_file pid + for state_file in "$LEGACY_PAUSED_STATE" "$LEGACY_OLD_PAUSED_STATE"; do + [[ -f "$state_file" ]] || continue + pid="$(sed -n 's/^pid=//p' "$state_file" | head -1)" + if valid_camera_pid "$pid" && camera_is_stopped "$pid"; then + kill -CONT "$pid" + alert RECOVERED "released legacy whole-process pause; detection and live stream remain running" + fi + rm -f "$state_file" + done } -pause_capture() { - local free_bytes="$1" pid - pid="$(camera_host_pid || true)" - if ! valid_camera_pid "$pid"; then - alert ERROR "cannot find smart_camera process; capture was not paused" - return 1 - fi - if camera_is_stopped "$pid"; then - printf 'pid=%s\npaused_at=%s\n' "$pid" "$(date --iso-8601=seconds)" > "$PAUSED_STATE" - start_fast_resume_checks - return 0 - fi - - if ! wait_for_safe_window; then - alert WARNING "no stable recording gap found; pausing capture to protect disk" - fi - kill -STOP "$pid" - printf 'pid=%s\npaused_at=%s\n' "$pid" "$(date --iso-8601=seconds)" > "$PAUSED_STATE" - start_fast_resume_checks - alert CRITICAL "capture paused at free=$(human_bytes "$free_bytes"); SFTP/Kafka uploader remains running; automatic resume target=$(human_bytes "$RESUME_TARGET_BYTES")" -} - -resume_capture_if_ready() { - local free_bytes="$1" pid - [[ -f "$PAUSED_STATE" ]] || return 0 - pid="$(sed -n 's/^pid=//p' "$PAUSED_STATE" | head -1)" - - if ! valid_camera_pid "$pid"; then - log WARNING "discarding stale capture pause state (pid=${pid:-unknown})" - rm -f "$PAUSED_STATE" - stop_fast_resume_checks - return 0 - fi - if ! camera_is_stopped "$pid"; then - log WARNING "capture is already running; clearing stale pause state" - rm -f "$PAUSED_STATE" - stop_fast_resume_checks - return 0 - fi - if [[ "$free_bytes" -ge "$RESUME_TARGET_BYTES" ]]; then - kill -CONT "$pid" - rm -f "$PAUSED_STATE" - stop_fast_resume_checks - alert RECOVERED "free space reached $(human_bytes "$free_bytes"); capture resumed automatically" +disable_recording() { + local free_bytes="$1" + if [[ ! -f "$RECORDING_PAUSE_MARKER" ]]; then + printf 'disabled_at=%s\nfree_bytes=%s\n' \ + "$(date --iso-8601=seconds)" "$free_bytes" > "$RECORDING_PAUSE_MARKER" + alert CRITICAL "local video recording disabled at free=$(human_bytes "$free_bytes"); videos/videos_raw/annotations stop growing; detection, RTSP/live stream and uploader remain running; automatic recording resume target=$(human_bytes "$RESUME_TARGET_BYTES")" else - log WARNING "capture remains paused: free=$(human_bytes "$free_bytes"), resume target=$(human_bytes "$RESUME_TARGET_BYTES")" + log WARNING "local video recording remains disabled: free=$(human_bytes "$free_bytes"), resume target=$(human_bytes "$RESUME_TARGET_BYTES")" + fi + start_fast_resume_checks +} + +enable_recording_if_ready() { + local free_bytes="$1" + [[ -f "$RECORDING_PAUSE_MARKER" ]] || { + stop_fast_resume_checks + return 0 + } + if [[ "$free_bytes" -ge "$RESUME_TARGET_BYTES" ]]; then + rm -f "$RECORDING_PAUSE_MARKER" + stop_fast_resume_checks + alert RECOVERED "free space reached $(human_bytes "$free_bytes"); local video recording enabled automatically; detection and live stream were never stopped" + else + log WARNING "local video recording remains disabled: free=$(human_bytes "$free_bytes"), resume target=$(human_bytes "$RESUME_TARGET_BYTES")" fi } @@ -145,24 +128,66 @@ load_processed_files() { if ! docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null | grep -qx true; then return 1 fi - docker exec "$CONTAINER" sh -c \ - 'test -r "$1" && cat "$1"' sh "$PROCESSED_FILE" 2>/dev/null | - sed -nE '/^video_[0-9]{8}_[0-9]{6}_[0-9]{3}\.xml$/p' | + docker exec -i "$CONTAINER" /opt/venv/bin/python - 2>/dev/null <<'PY' | +import json +import re + +processed_path = "/smart-cam/kafka_sender/processed_files.log" +config_path = "/smart-cam/config/config.json" +receipts_path = "/smart-cam/data/upload_completed_types.json" +legacy_types = {"videos_raw", "annotations"} +pattern = re.compile(r"^video_[0-9]{8}_[0-9]{6}_[0-9]{3}\.xml$") + +with open(config_path, encoding="utf-8") as stream: + upload = json.load(stream).get("upload", {}) +required = { + name for name, default in ( + ("videos", False), ("videos_raw", True), ("annotations", True) + ) if bool(upload.get(name, default)) +} +try: + with open(receipts_path, encoding="utf-8") as stream: + receipts = json.load(stream) +except (FileNotFoundError, json.JSONDecodeError, OSError): + receipts = {} + +with open(processed_path, encoding="utf-8") as stream: + for line in stream: + name = line.strip() + if not pattern.fullmatch(name): + continue + confirmed = set(receipts.get(name, {}).get("types", legacy_types)) + if required.issubset(confirmed): + print(name) +PY sort -u > "$destination" [[ -s "$destination" ]] } build_safe_bundles() { - local processed="$1" output="$2" xml base raw annotated newest_ts + local processed="$1" output="$2" xml base raw annotated newest_ts path complete : > "$output" while IFS= read -r xml; do base="${xml%.xml}" raw="$DATA_ROOT/videos_raw/${base}_raw.mp4" annotated="$DATA_ROOT/videos/${base}_annotated.mp4" - # The raw file is the authoritative large asset. Only select completed, - # sufficiently old bundles. Missing members are harmless and skipped later. - [[ -f "$raw" ]] || continue - find "$raw" -mmin "+${MIN_FILE_AGE_MINUTES}" -print -quit 2>/dev/null | grep -q . || continue + complete=1 + for path in "$raw" "$annotated" "$DATA_ROOT/annotations/${base}.xml"; do + [[ -f "$path" ]] || { + complete=0 + break + } + find "$path" -mmin "+${MIN_FILE_AGE_MINUTES}" -print -quit 2>/dev/null | + grep -q . || { + complete=0 + break + } + if command -v fuser >/dev/null 2>&1 && fuser -s "$path" 2>/dev/null; then + complete=0 + break + fi + done + [[ "$complete" -eq 1 ]] || continue newest_ts="$(stat -c %Y "$raw" 2>/dev/null || printf 0)" printf '%s %s\n' "$newest_ts" "$base" >> "$output" done < "$processed" @@ -170,9 +195,23 @@ build_safe_bundles() { } delete_safe_bundles() { - local bundles="$1" deleted_count=0 deleted_bytes=0 record base path size + local bundles="$1" deleted_count=0 deleted_bytes=0 record base path size complete while IFS= read -r record; do base="${record#* }" + complete=1 + for path in \ + "$DATA_ROOT/videos_raw/${base}_raw.mp4" \ + "$DATA_ROOT/videos/${base}_annotated.mp4" \ + "$DATA_ROOT/annotations/${base}.xml"; do + [[ -f "$path" ]] || { + complete=0 + break + } + done + if [[ "$complete" -ne 1 ]]; then + log WARNING "confirmed bundle became incomplete before deletion; skipped as a unit: ${base}" + continue + fi for path in \ "$DATA_ROOT/videos_raw/${base}_raw.mp4" \ "$DATA_ROOT/videos/${base}_annotated.mp4" \ @@ -191,6 +230,152 @@ delete_safe_bundles() { log INFO "safe cleanup result: deleted=${deleted_count} files bytes=${deleted_bytes} free=$(human_bytes "$(available_bytes)")" } +delete_confirmed_scans() { + local scan_id scan_dir resolved size + local deleted_scans=0 deleted_bytes=0 unsafe + + [[ -r "$SCAN_PROCESSED_FILE" ]] || { + log INFO "no confirmed scan upload list yet; scan cleanup skipped" + return 0 + } + + while IFS= read -r scan_id; do + [[ "$scan_id" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2}$ ]] || + continue + scan_dir="$DATA_ROOT/scan/$scan_id" + [[ -d "$scan_dir" ]] || continue + + resolved="$(readlink -f -- "$scan_dir" 2>/dev/null || true)" + [[ "$resolved" == "$DATA_ROOT/scan/$scan_id" ]] || { + log WARNING "scan path failed safety check; skipped: ${scan_dir}" + continue + } + + # A processed marker is only written after every image/meta SFTP upload + # and the Kafka delivery callback have succeeded. Re-check that this is a + # completed scan and that it has aged out before deleting the directory. + if ! python3 - "$scan_dir/meta.json" <<'PY' +import json +import sys +try: + with open(sys.argv[1], encoding="utf-8") as stream: + meta = json.load(stream) + state = meta.get("state", {}) + images = meta.get("images", []) + total = int(state.get("total", 0)) + valid = ( + state.get("state") == "completed" + and total > 0 + and int(state.get("completed", -1)) == total + and len(images) == total + ) +except (OSError, ValueError, TypeError, json.JSONDecodeError): + valid = False +raise SystemExit(0 if valid else 1) +PY + then + log WARNING "confirmed scan has invalid/incomplete meta; skipped: ${scan_id}" + continue + fi + + if find "$scan_dir" -xdev -type f \ + ! -mmin "+${MIN_FILE_AGE_MINUTES}" -print -quit 2>/dev/null | + grep -q .; then + continue + fi + + unsafe=0 + if command -v fuser >/dev/null 2>&1; then + while IFS= read -r -d '' path; do + if fuser -s "$path" 2>/dev/null; then + log WARNING "confirmed scan file is still open; skipped scan=${scan_id} file=${path}" + unsafe=1 + break + fi + done < <(find "$scan_dir" -xdev -type f -print0 2>/dev/null) + fi + [[ "$unsafe" -eq 0 ]] || continue + + size="$(du -sb "$scan_dir" 2>/dev/null | awk '{print $1}')" + size="${size:-0}" + if find "$scan_dir" -xdev -depth -delete 2>/dev/null && + [[ ! -e "$scan_dir" ]]; then + deleted_scans=$((deleted_scans + 1)) + deleted_bytes=$((deleted_bytes + size)) + log INFO "deleted confirmed uploaded scan (${size} bytes): ${scan_dir}" + else + log ERROR "failed to delete confirmed uploaded scan: ${scan_dir}" + fi + done < <(sort -u "$SCAN_PROCESSED_FILE") + + log INFO "scan cleanup result: deleted=${deleted_scans} scans bytes=${deleted_bytes}" +} + +delete_stale_incomplete_bundles() { + local bases base filename raw annotated xml path + local present safe size deleted_bundles=0 deleted_files=0 deleted_bytes=0 + bases="$(mktemp /tmp/userdata-incomplete-bases.XXXXXX)" + + find "$DATA_ROOT/videos_raw" -maxdepth 1 -type f -name 'video_*_raw.mp4' \ + -printf '%f\n' 2>/dev/null | + sed 's/_raw\.mp4$//' >> "$bases" + find "$DATA_ROOT/videos" -maxdepth 1 -type f -name 'video_*_annotated.mp4' \ + -printf '%f\n' 2>/dev/null | + sed 's/_annotated\.mp4$//' >> "$bases" + find "$DATA_ROOT/annotations" -maxdepth 1 -type f -name 'video_*.xml' \ + -printf '%f\n' 2>/dev/null | + sed 's/\.xml$//' >> "$bases" + sort -u "$bases" -o "$bases" + + while IFS= read -r base; do + [[ "$base" =~ ^video_[0-9]{8}_[0-9]{6}_[0-9]{3}$ ]] || continue + raw="$DATA_ROOT/videos_raw/${base}_raw.mp4" + annotated="$DATA_ROOT/videos/${base}_annotated.mp4" + xml="$DATA_ROOT/annotations/${base}.xml" + present=0 + [[ -f "$raw" ]] && present=$((present + 1)) + [[ -f "$annotated" ]] && present=$((present + 1)) + [[ -f "$xml" ]] && present=$((present + 1)) + [[ "$present" -gt 0 && "$present" -lt 3 ]] || continue + + # Recording creates members at slightly different times. Only reconcile + # bundles when every existing member is old and no process has it open. + safe=1 + for path in "$raw" "$annotated" "$xml"; do + [[ -f "$path" ]] || continue + find "$path" -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 "$path" 2>/dev/null; then + log WARNING "incomplete bundle member is still open; skipped bundle=${base} file=${path}" + safe=0 + break + fi + done + [[ "$safe" -eq 1 ]] || continue + + for path in "$raw" "$annotated" "$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}" + else + log ERROR "failed to delete stale incomplete bundle member: ${path}" + fi + done + deleted_bundles=$((deleted_bundles + 1)) + done < "$bases" + rm -f -- "$bases" + + if [[ "$deleted_bundles" -gt 0 ]]; then + log INFO "incomplete bundle reconciliation result: bundles=${deleted_bundles} files=${deleted_files} bytes=${deleted_bytes}" + fi +} + delete_stale_zero_bundles() { local raw base path deleted_bundles=0 deleted_files=0 while IFS= read -r -d '' raw; do @@ -313,19 +498,21 @@ delete_stale_zero_part_bundles() { main() { local free_bytes processed bundles + recover_legacy_capture_pause delete_stale_zero_bundles delete_stale_zero_part_bundles + delete_stale_incomplete_bundles free_bytes="$(available_bytes)" - # Protect the disk before cleanup work: once free space is below 3 GiB, - # stop continuous detection/recording/inference first while upload stays up. + # Protect the disk before cleanup work. Only recording is disabled; continuous + # detection, inference, RTSP/live output and upload remain available. if [[ "$free_bytes" -lt "$CRITICAL_BELOW_BYTES" ]]; then alert CRITICAL "free space below critical threshold: $(human_bytes "$free_bytes")" fi if [[ "$free_bytes" -lt "$PAUSE_BELOW_BYTES" ]]; then - pause_capture "$free_bytes" + disable_recording "$free_bytes" fi - resume_capture_if_ready "$free_bytes" + enable_recording_if_ready "$free_bytes" processed="$(mktemp /tmp/userdata-processed.XXXXXX)" bundles="$(mktemp /tmp/userdata-bundles.XXXXXX)" @@ -342,14 +529,18 @@ main() { alert ERROR "cannot read confirmed-upload list; fail-safe mode prevents all deletion" fi + delete_confirmed_scans + free_bytes="$(available_bytes)" if [[ "$free_bytes" -lt "$CRITICAL_BELOW_BYTES" ]]; then alert CRITICAL "free space below critical threshold: $(human_bytes "$free_bytes")" fi if [[ "$free_bytes" -lt "$PAUSE_BELOW_BYTES" ]]; then - pause_capture "$free_bytes" + disable_recording "$free_bytes" fi - resume_capture_if_ready "$(available_bytes)" + enable_recording_if_ready "$(available_bytes)" } -main "$@" +if [[ "${CLEANUP_SMARTCAM_LIB_ONLY:-0}" != "1" ]]; then + main "$@" +fi diff --git a/scripts/install.sh b/scripts/install.sh index 526637a..9d1849d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -15,11 +15,6 @@ bash -n "$ROOT_DIR/bin/cleanup-smartcam-data" systemctl disable --now userdata-video-cleanup.timer 2>/dev/null || true install -d -o root -g root -m 0755 /var/lib/cleanup-smartcam-data -if [[ -f /var/lib/userdata-video-cleanup/capture-paused && \ - ! -f /var/lib/cleanup-smartcam-data/capture-paused ]]; then - mv /var/lib/userdata-video-cleanup/capture-paused \ - /var/lib/cleanup-smartcam-data/capture-paused -fi install -o root -g root -m 0755 \ "$ROOT_DIR/bin/cleanup-smartcam-data" \ @@ -40,9 +35,11 @@ install -o root -g root -m 0644 \ systemctl daemon-reload systemctl enable --now cleanup-smartcam-data.timer -# The fast timer is started by the cleanup program only while capture is paused. +# The fast timer is started only while low disk space disables local recording. systemctl disable cleanup-smartcam-data-resume.timer 2>/dev/null || true -if [[ -f /var/lib/cleanup-smartcam-data/capture-paused ]]; then +if [[ -f /userdata/data/.recording-paused-low-disk || \ + -f /var/lib/cleanup-smartcam-data/capture-paused || \ + -f /var/lib/userdata-video-cleanup/capture-paused ]]; then systemctl start cleanup-smartcam-data-resume.timer else systemctl stop cleanup-smartcam-data-resume.timer 2>/dev/null || true @@ -59,5 +56,5 @@ echo "安装完成。" echo "程序: /usr/local/sbin/cleanup-smartcam-data" echo "服务: /etc/systemd/system/cleanup-smartcam-data.service" echo "定时器: /etc/systemd/system/cleanup-smartcam-data.timer" -echo "暂停恢复定时器: /etc/systemd/system/cleanup-smartcam-data-resume.timer" +echo "录像恢复定时器: /etc/systemd/system/cleanup-smartcam-data-resume.timer" systemctl --no-pager status cleanup-smartcam-data.timer || true diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 15ad381..0291a6a 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -16,4 +16,4 @@ rm -f \ systemctl daemon-reload echo "cleanup-smartcam-data 已卸载。" -echo "未删除 /var/lib/cleanup-smartcam-data;请确认采集未处于暂停状态后再人工处理该目录。" +echo "未删除 /userdata/data/.recording-paused-low-disk;如需恢复录像,请确认空间充足后人工删除。" diff --git a/systemd/cleanup-smartcam-data-resume.timer b/systemd/cleanup-smartcam-data-resume.timer index c9d5293..18282ee 100644 --- a/systemd/cleanup-smartcam-data-resume.timer +++ b/systemd/cleanup-smartcam-data-resume.timer @@ -1,5 +1,5 @@ [Unit] -Description=Check smart-cam disk recovery every 2 minutes while capture is paused +Description=Check smart-cam disk recovery every 2 minutes while recording is disabled Documentation=man:systemd.timer(5) [Timer] @@ -7,4 +7,3 @@ OnActiveSec=1min OnUnitActiveSec=2min AccuracySec=15s Unit=cleanup-smartcam-data.service - diff --git a/tests/scan-processed-fixture.log b/tests/scan-processed-fixture.log new file mode 100644 index 0000000..4bbb5da --- /dev/null +++ b/tests/scan-processed-fixture.log @@ -0,0 +1 @@ +2026-07-25_16-00-08