"""Очистка архива по квоте диска и по сроку хранения.""" 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: # квота — на КАЖДОЕ хранилище отдельно (0 = без лимита). Для каждого удаляем # самые старые записи ВНУТРИ него, пока его размер не уложится в его квоту. removed = 0 for item in self.config.storage.items: if not item.quota_gb: continue quota = item.quota_gb * 1024 ** 3 while await self.db.total_size_under(item.path) > quota: batch = await self.db.oldest_recordings_under(item.path, 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_under(item.path) <= 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