Normalize recording roots to configured storage paths

This commit is contained in:
ibs0d 2026-03-07 21:27:07 +11:00
parent 14da24e0fa
commit 7368ee715b
2 changed files with 97 additions and 0 deletions

View File

@ -1,6 +1,7 @@
"""Handle storage retention and usage.""" """Handle storage retention and usage."""
import logging import logging
import re
import shutil import shutil
import threading import threading
from pathlib import Path from pathlib import Path
@ -178,6 +179,22 @@ class StorageMaintainer(threading.Thread):
if camera_segment in recording_path: if camera_segment in recording_path:
return recording_path.split(camera_segment, 1)[0].rstrip("/") or "/" return recording_path.split(camera_segment, 1)[0].rstrip("/") or "/"
# Prefer configured recording roots when available.
for configured_root in sorted(
self.config.get_recordings_paths(), key=len, reverse=True
):
if recording_path == configured_root or recording_path.startswith(
f"{configured_root}/"
):
return configured_root.rstrip("/") or "/"
# Support layouts like /root/YYYY-MM-DD/HH/... and normalize to /root.
date_hour_match = re.match(
r"^(?P<root>.+?)/\d{4}-\d{2}-\d{2}/\d{2}(?:/|$)", recording_path
)
if date_hour_match:
return date_hour_match.group("root").rstrip("/") or "/"
# Fallback for unexpected path layouts; expected format is root/camera/date/file # Fallback for unexpected path layouts; expected format is root/camera/date/file
path = Path(recording_path) path = Path(recording_path)
if len(path.parents) >= 3: if len(path.parents) >= 3:

View File

@ -227,3 +227,83 @@ class TestHttpRecordingsStorage(BaseTestHttp):
# Config metadata still reflects current root assignment. # Config metadata still reflects current root assignment.
assert roots["/media/frigate/recordings"]["configured_cameras"] == [] assert roots["/media/frigate/recordings"]["configured_cameras"] == []
assert roots["/mnt/new-root"]["configured_cameras"] == ["front_door"] assert roots["/mnt/new-root"]["configured_cameras"] == ["front_door"]
def test_recordings_storage_normalizes_date_hour_paths_to_configured_roots(self):
self.minimal_config["cameras"]["front_door"]["path"] = "/media/frigate/recordings"
self.minimal_config["cameras"]["back_yard"] = {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
"path": "/video1",
}
self.test_stats["service"]["storage"]["/video1"] = {
"free": 600,
"mount_type": "ext4",
"total": 1000,
"used": 400,
}
app = self._build_app()
Recordings.insert_many(
[
{
"id": "front_08",
"path": "/media/frigate/recordings/2026-03-07/08/seg1.mp4",
"camera": "front_door",
"start_time": 100,
"end_time": 110,
"duration": 10,
"motion": 1,
"segment_size": 100,
},
{
"id": "front_09",
"path": "/media/frigate/recordings/2026-03-07/09/seg1.mp4",
"camera": "front_door",
"start_time": 120,
"end_time": 130,
"duration": 10,
"motion": 1,
"segment_size": 150,
},
{
"id": "video1_08",
"path": "/video1/2026-03-07/08/seg1.mp4",
"camera": "back_yard",
"start_time": 140,
"end_time": 150,
"duration": 10,
"motion": 1,
"segment_size": 200,
},
{
"id": "video1_09",
"path": "/video1/2026-03-07/09/seg1.mp4",
"camera": "back_yard",
"start_time": 160,
"end_time": 170,
"duration": 10,
"motion": 1,
"segment_size": 50,
},
]
).execute()
with AuthTestClient(app) as client:
payload = client.get("/recordings/storage").json()
roots = {root["path"]: root for root in payload["recording_roots"]}
assert set(roots.keys()) == {"/media/frigate/recordings", "/video1"}
assert roots["/media/frigate/recordings"]["recordings_size"] == 250
assert roots["/video1"]["recordings_size"] == 250
# Ensure date/hour pseudo-roots are not emitted.
assert "/media/frigate/recordings/2026-03-07/08" not in roots
assert "/media/frigate/recordings/2026-03-07/09" not in roots
assert "/video1/2026-03-07/08" not in roots
assert "/video1/2026-03-07/09" not in roots