mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-08-13 13:47:36 +02:00
Add retain.max_size to clean up storage once the specified limit has been reached
This commit is contained in:
parent
c3410cd13e
commit
189adf9d30
@ -70,6 +70,8 @@ record:
|
||||
|
||||
As of Frigate 0.12 if there is less than an hour left of storage, the oldest 2 hours of recordings will be deleted.
|
||||
|
||||
Additionally, you can configure a maximum storage size limit for recordings using the `max_size` option. When the total size of recordings exceeds this limit, Frigate will automatically clean up the oldest recordings to stay within the specified size.
|
||||
|
||||
## Configuring Recording Retention
|
||||
|
||||
Frigate supports both continuous and tracked object based recordings with separate retention modes and retention periods.
|
||||
@ -93,6 +95,20 @@ record:
|
||||
|
||||
Continuous recording supports different retention modes [which are described below](#what-do-the-different-retain-modes-mean)
|
||||
|
||||
### Storage Size Limit
|
||||
|
||||
You can configure a maximum storage size limit for recordings. When the total size of recordings exceeds this limit, Frigate will automatically clean up the oldest recordings first.
|
||||
|
||||
```yaml
|
||||
record:
|
||||
enabled: True
|
||||
retain:
|
||||
days: 7
|
||||
max_size: 5000 # <- maximum storage size in MB (5GB in this example)
|
||||
```
|
||||
|
||||
The `max_size` parameter specifies the maximum total storage size in megabytes (MB) that recordings should consume. This works in addition to the time-based retention settings - recordings will be deleted when either the time limit OR the size limit is exceeded.
|
||||
|
||||
### Object Recording
|
||||
|
||||
The number of days to record review items can be specified for review items classified as alerts as well as tracked objects.
|
||||
|
@ -452,6 +452,9 @@ record:
|
||||
# active_objects - save all recording segments with active/moving objects
|
||||
# NOTE: this mode only applies when the days setting above is greater than 0
|
||||
mode: all
|
||||
# Optional: Maximum storage size in MB for recordings (default: no limit)
|
||||
# When total recording storage exceeds this limit, oldest recordings will be deleted
|
||||
max_size: 5000
|
||||
# Optional: Recording Export Settings
|
||||
export:
|
||||
# Optional: Timelapse Output Args (default: shown below).
|
||||
|
@ -31,6 +31,7 @@ class RetainModeEnum(str, Enum):
|
||||
class RecordRetainConfig(FrigateBaseModel):
|
||||
days: float = Field(default=0, title="Default retention period.")
|
||||
mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
|
||||
max_size: Optional[int] = Field(default=None, title="Maximum storage size in MB.")
|
||||
|
||||
|
||||
class ReviewRetainConfig(FrigateBaseModel):
|
||||
|
@ -94,6 +94,22 @@ class StorageMaintainer(threading.Thread):
|
||||
[b["bandwidth"] for b in self.camera_storage_stats.values()]
|
||||
)
|
||||
remaining_storage = round(shutil.disk_usage(RECORD_DIR).free / pow(2, 20), 1)
|
||||
|
||||
# Check if max_size is configured and total usage exceeds it
|
||||
max_size = self.config.record.retain.max_size
|
||||
if max_size is not None:
|
||||
total_usage = (
|
||||
Recordings.select(fn.SUM(Recordings.segment_size))
|
||||
.where(Recordings.segment_size != 0)
|
||||
.scalar() or 0
|
||||
) / pow(2, 20) # Convert to MB
|
||||
|
||||
if total_usage > max_size:
|
||||
logger.debug(
|
||||
f"Storage cleanup check: total usage {total_usage} MB exceeds max_size {max_size} MB."
|
||||
)
|
||||
return True
|
||||
|
||||
logger.debug(
|
||||
f"Storage cleanup check: {hourly_bandwidth} hourly with remaining storage: {remaining_storage}."
|
||||
)
|
||||
|
@ -1531,6 +1531,49 @@ class TestConfig(unittest.TestCase):
|
||||
|
||||
self.assertRaises(ValueError, lambda: FrigateConfig(**config))
|
||||
|
||||
def test_record_retain_max_size(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"record": {"retain": {"max_size": 1000}},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert frigate_config.record.retain.max_size == 1000
|
||||
|
||||
def test_record_retain_max_size_default(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert frigate_config.record.retain.max_size is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
@ -260,6 +260,78 @@ class TestHttp(unittest.TestCase):
|
||||
assert Recordings.get(Recordings.id == rec_k2_id)
|
||||
assert Recordings.get(Recordings.id == rec_k3_id)
|
||||
|
||||
def test_storage_cleanup_with_max_size(self):
|
||||
"""Test that storage cleanup is triggered when max_size is exceeded."""
|
||||
config_with_max_size = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"record": {"retain": {"max_size": 1}}, # 1 MB max size
|
||||
"cameras": {
|
||||
"front_door": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
config = FrigateConfig(**config_with_max_size)
|
||||
storage = StorageMaintainer(config, MagicMock())
|
||||
|
||||
# Insert recordings that exceed max_size (1 MB = 1048576 bytes)
|
||||
time_now = datetime.datetime.now().timestamp()
|
||||
rec_id1 = "test1.recording"
|
||||
rec_id2 = "test2.recording"
|
||||
|
||||
# Create recordings with size larger than max_size
|
||||
_insert_mock_recording(
|
||||
rec_id1,
|
||||
os.path.join(self.test_dir, f"{rec_id1}.tmp"),
|
||||
time_now - 3600,
|
||||
time_now - 3590,
|
||||
seg_size=524288, # 0.5 MB
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_id2,
|
||||
os.path.join(self.test_dir, f"{rec_id2}.tmp"),
|
||||
time_now - 1800,
|
||||
time_now - 1790,
|
||||
seg_size=524288, # 0.5 MB (total 1 MB)
|
||||
)
|
||||
|
||||
storage.calculate_camera_bandwidth()
|
||||
|
||||
# Should trigger cleanup since total size (1 MB) exceeds max_size (1 MB)
|
||||
assert storage.check_storage_needs_cleanup() == True
|
||||
|
||||
def test_storage_cleanup_without_max_size(self):
|
||||
"""Test that max_size check is skipped when not configured."""
|
||||
config = FrigateConfig(**self.minimal_config)
|
||||
storage = StorageMaintainer(config, MagicMock())
|
||||
|
||||
time_now = datetime.datetime.now().timestamp()
|
||||
rec_id = "test.recording"
|
||||
|
||||
# Create a large recording
|
||||
_insert_mock_recording(
|
||||
rec_id,
|
||||
os.path.join(self.test_dir, f"{rec_id}.tmp"),
|
||||
time_now - 3600,
|
||||
time_now - 3590,
|
||||
seg_size=1048576, # 1 MB
|
||||
)
|
||||
|
||||
storage.calculate_camera_bandwidth()
|
||||
|
||||
# Should not trigger cleanup based on max_size since it's not configured
|
||||
# (may still trigger based on free space, but that's a separate check)
|
||||
assert storage.config.record.retain.max_size is None
|
||||
|
||||
|
||||
def _insert_mock_event(id: str, start: int, end: int, retain: bool) -> Event:
|
||||
"""Inserts a basic event model with a given id."""
|
||||
|
Loading…
Reference in New Issue
Block a user