Files
CoRE.Vision/backend/app/services/retention.py
T
2026-05-30 00:08:59 +05:00

102 lines
3.2 KiB
Python

"""Очистка архива по квоте диска и по сроку хранения."""
from __future__ import annotations
import asyncio
import logging
import os
import time
from ..config import Config
from ..db import Database
log = logging.getLogger("nvr.retention")
CHECK_INTERVAL = 300 # сек
class Retention:
def __init__(self, config: Config, db: Database):
self.config = config
self.db = db
self._task: asyncio.Task | None = None
self._stopping = False
def start(self) -> None:
self._stopping = False
self._task = asyncio.create_task(self._loop(), name="retention")
async def stop(self) -> None:
self._stopping = True
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
async def _loop(self) -> None:
while not self._stopping:
try:
await self.enforce()
except Exception: # noqa: BLE001
log.exception("retention failed")
await asyncio.sleep(CHECK_INTERVAL)
async def enforce(self) -> int:
removed = 0
removed += await self._enforce_age()
removed += await self._enforce_quota()
if removed:
log.info("retention: удалено сегментов %d", removed)
return removed
async def _enforce_age(self) -> int:
days = self.config.storage.retention_days
if not days:
return 0
cutoff = int(time.time()) - days * 86400
old = await self.db.recordings_before(cutoff)
for rec in old:
await self._remove(rec.id, rec.path)
return len(old)
async def _enforce_quota(self) -> int:
quota = self.config.storage.quota_gb * 1024 ** 3
if not quota:
return 0
removed = 0
# удаляем самые старые, пока суммарный размер не уложится в квоту
while await self.db.total_size() > quota:
batch = await self.db.oldest_recordings(limit=20)
if not batch:
break
for rec in batch:
await self._remove(rec.id, rec.path)
removed += 1
if await self.db.total_size() <= quota:
break
return removed
async def _remove(self, rec_id: int, path: str) -> None:
try:
os.remove(path)
except FileNotFoundError:
pass
except OSError as exc:
log.warning("не удалось удалить %s: %s", path, exc)
await self.db.delete_recording(rec_id)
_cleanup_empty_dirs(os.path.dirname(path))
def _cleanup_empty_dirs(path: str) -> None:
"""Удаляет пустой каталог дня (и камеры), оставшийся после очистки."""
for _ in range(2):
try:
if path and os.path.isdir(path) and not os.listdir(path):
os.rmdir(path)
path = os.path.dirname(path)
else:
break
except OSError:
break