diff --git a/frigate/jobs/export.py b/frigate/jobs/export.py index e1cf09060e..abc2e5837f 100644 --- a/frigate/jobs/export.py +++ b/frigate/jobs/export.py @@ -15,7 +15,7 @@ from peewee import DoesNotExist from frigate.comms.inter_process import InterProcessRequestor from frigate.config import FrigateConfig from frigate.config.camera.record import ChaptersEnum -from frigate.const import UPDATE_JOB_STATE +from frigate.const import EXPORT_DIR, UPDATE_JOB_STATE from frigate.jobs.job import Job from frigate.models import Export from frigate.record.export import ( @@ -415,6 +415,10 @@ def reap_stale_exports() -> None: this in a try/except. A failure on a single row will not stop the rest of the sweep, and a failure in the top-level query will log and return. """ + # staged stream runs live on disk, so a killed export leaves them behind + for staged in Path(EXPORT_DIR).glob("export_stage_*"): + staged.unlink(missing_ok=True) + try: stale_exports = list(Export.select().where(Export.in_progress == True)) # noqa: E712 except Exception: diff --git a/frigate/record/export.py b/frigate/record/export.py index 7455d56b05..4446dbf5f3 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -559,7 +559,7 @@ class RecordingExporter(threading.Thread): ) def _staged_run_path(self, index: int) -> str: - return os.path.join(CACHE_DIR, f"export_stage_{self.export_id}_{index}.mp4") + return os.path.join(EXPORT_DIR, f"export_stage_{self.export_id}_{index}.mp4") def _probe_stream_resolution(self, run: StreamRun) -> tuple[int, int] | None: """Probe one recording from a run for its resolution. diff --git a/frigate/test/http_api/test_http_export.py b/frigate/test/http_api/test_http_export.py index 51f10c7abb..58c48f4056 100644 --- a/frigate/test/http_api/test_http_export.py +++ b/frigate/test/http_api/test_http_export.py @@ -453,6 +453,21 @@ class TestHttpExport(BaseTestHttp): assert unchanged.name == "front door export" assert unchanged.video_path == video + def test_reap_stale_exports_removes_staged_runs(self): + with tempfile.TemporaryDirectory() as tmpdir: + staged = os.path.join(tmpdir, "export_stage_front_door_abc_0.mp4") + finished = os.path.join(tmpdir, "front_door_export.mp4") + + for path in (staged, finished): + with open(path, "w") as handle: + handle.write("video") + + with patch("frigate.jobs.export.EXPORT_DIR", tmpdir): + reap_stale_exports() + + assert not os.path.exists(staged) + assert os.path.exists(finished) + def test_reap_stale_exports_deletes_rows_with_no_file(self): with tempfile.TemporaryDirectory() as tmpdir: stale_video = os.path.join(tmpdir, "stale.mp4")