storage: multi-storage management with per-storage quota
Replace the single fixed /records path with a list of storages and an
active selection (where new recordings go). Quota is now per-storage.
- config: StorageItem(id,name,type,path,quota_gb,nas-meta) + Storage keeps
global retention_days/segment_seconds; active + items. Migration from the
old single {path,quota_gb} schema → one local item.
- recorder: write to the active storage; restart_all() repoints on switch;
NAS-not-mounted guard (never write into the container overlay).
- indexer: scan ALL storage roots so recordings survive an active switch.
- retention: enforce quota PER storage via db.total_size_under /
oldest_recordings_under (path-prefix LIKE).
- api/storages: GET/POST/PUT/DELETE + activate + policy. NAS uses host-mount
(backend stores SMB params, generates the mount/fstab command, reports
ismount status). Retire old POST /api/storage.
- frontend: new "Хранилища" settings tab — table with inline per-row quota,
add form (local folder / NAS SMB), global policy (segment + retention).
- compose: add shared ./storages:/storages root for extra local folders and
host-mounted NAS shares.
VERSION 0.0.127.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,20 +61,22 @@ class Retention:
|
||||
return len(old)
|
||||
|
||||
async def _enforce_quota(self) -> int:
|
||||
quota = self.config.storage.quota_gb * 1024 ** 3
|
||||
if not quota:
|
||||
return 0
|
||||
# квота — на КАЖДОЕ хранилище отдельно (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:
|
||||
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:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Системная статистика: диск хранилища, CPU, RAM, аптайм."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
@@ -11,19 +12,31 @@ from ..config import Config
|
||||
_START = time.time()
|
||||
|
||||
|
||||
def system_stats(config: Config) -> dict:
|
||||
path = config.storage.path
|
||||
def storage_disk(path: str) -> dict:
|
||||
"""Использование диска, на котором лежит path (для одного хранилища)."""
|
||||
try:
|
||||
usage = shutil.disk_usage(path)
|
||||
disk = {
|
||||
return {
|
||||
"total_gb": round(usage.total / 1024 ** 3, 1),
|
||||
"used_gb": round(usage.used / 1024 ** 3, 1),
|
||||
"free_gb": round(usage.free / 1024 ** 3, 1),
|
||||
"percent": round(usage.used / usage.total * 100, 1) if usage.total else 0,
|
||||
}
|
||||
except OSError:
|
||||
disk = {"total_gb": 0, "used_gb": 0, "free_gb": 0, "percent": 0}
|
||||
return {"total_gb": 0, "used_gb": 0, "free_gb": 0, "percent": 0}
|
||||
|
||||
|
||||
def is_mounted(path: str) -> bool:
|
||||
"""Точка монтирования? (для NAS — признак, что том реально подключён)."""
|
||||
try:
|
||||
return os.path.ismount(path)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def system_stats(config: Config) -> dict:
|
||||
active = config.active_storage()
|
||||
path = active.path
|
||||
vm = psutil.virtual_memory()
|
||||
return {
|
||||
"uptime_s": int(time.time() - _START),
|
||||
@@ -32,8 +45,8 @@ def system_stats(config: Config) -> dict:
|
||||
"ram_used_gb": round(vm.used / 1024 ** 3, 1),
|
||||
"ram_total_gb": round(vm.total / 1024 ** 3, 1),
|
||||
"storage_path": path,
|
||||
"disk": disk,
|
||||
"quota_gb": config.storage.quota_gb,
|
||||
"disk": storage_disk(path),
|
||||
"quota_gb": active.quota_gb,
|
||||
"retention_days": config.storage.retention_days,
|
||||
"segment_seconds": config.storage.segment_seconds,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user