init
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Очистка архива по квоте диска и по сроку хранения."""
|
||||
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
|
||||
Reference in New Issue
Block a user