From 6248dbf12f0ad84e464aafc0473942c0f417a265 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 08:51:42 +0000 Subject: [PATCH] Delete preview files when emergency storage cleanup removes recordings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When reduce_storage_consumption deletes old recording segments to free disk space, it now also deletes preview files that overlap the same time range. Without this, preview mp4 files on the same disk continued to consume space, causing the storage maintainer to delete progressively newer recordings while old previews accumulated — resulting in archives where older periods had previews but no video. This is particularly impactful for multi-path setups where each camera's preview directory shares a disk with its recordings. https://claude.ai/code/session_016bxjbVpx8DqpjysnGYmXdx --- frigate/storage.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/frigate/storage.py b/frigate/storage.py index 9f92ceb7b..9bcec9c4c 100644 --- a/frigate/storage.py +++ b/frigate/storage.py @@ -10,7 +10,7 @@ from peewee import SQL, fn from frigate.config import FrigateConfig from frigate.const import RECORD_DIR, REPLAY_CAMERA_PREFIX -from frigate.models import Event, Recordings +from frigate.models import Event, Previews, Recordings from frigate.util.builtin import clear_and_unlink logger = logging.getLogger(__name__) @@ -390,6 +390,32 @@ class StorageMaintainer(threading.Thread): f"Updated has_clip to False for {len(events_to_update)} events" ) + # Also delete preview files that overlap with deleted recordings so they + # don't continue to consume space on the same disk after the recordings + # are gone (especially important for multi-path setups where preview and + # recordings share the same disk). + if deleted_recordings: + deleted_previews = [] + for camera, time_range in camera_recordings.items(): + overlapping_previews = ( + Previews.select(Previews.id, Previews.path) + .where( + Previews.camera == camera, + Previews.start_time < time_range["max_end"], + Previews.end_time > time_range["min_start"], + ) + .namedtuples() + ) + for preview in overlapping_previews: + clear_and_unlink(Path(preview.path), missing_ok=True) + deleted_previews.append(preview.id) + + logger.debug(f"Expiring {len(deleted_previews)} previews") + for i in range(0, len(deleted_previews), max_deletes): + Previews.delete().where( + Previews.id << deleted_previews[i : i + max_deletes] + ).execute() + deleted_recordings_list = [r.id for r in deleted_recordings] for i in range(0, len(deleted_recordings_list), max_deletes): Recordings.delete().where(