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:
@@ -80,24 +80,28 @@ class Indexer:
|
||||
await asyncio.sleep(SCAN_INTERVAL)
|
||||
|
||||
async def scan_once(self) -> int:
|
||||
root = self.config.storage.path
|
||||
by_cam = _scan_segments(root)
|
||||
active_path = self.config.active_storage().path
|
||||
known = await self.db.known_paths()
|
||||
added = 0
|
||||
|
||||
for cam_id, files in by_cam.items():
|
||||
# последний файл может писаться прямо сейчас — не индексируем
|
||||
complete = files[:-1] if len(files) > 1 else []
|
||||
for path in complete:
|
||||
if path in known:
|
||||
continue
|
||||
duration, size = await probe_duration(path)
|
||||
if size is None or size == 0:
|
||||
continue
|
||||
await self.db.add_recording(
|
||||
cam_id, path, _parse_started_at(path), duration, size
|
||||
)
|
||||
added += 1
|
||||
# сканируем ВСЕ хранилища: после смены активного старые записи не теряются
|
||||
for root in self.config.storage_paths():
|
||||
by_cam = _scan_segments(root)
|
||||
is_active = (root == active_path)
|
||||
for cam_id, files in by_cam.items():
|
||||
# в активном хранилище последний файл ещё пишется — пропускаем;
|
||||
# в неактивных все сегменты завершены, индексируем целиком.
|
||||
complete = files[:-1] if is_active else files
|
||||
for path in complete:
|
||||
if path in known:
|
||||
continue
|
||||
duration, size = await probe_duration(path)
|
||||
if size is None or size == 0:
|
||||
continue
|
||||
await self.db.add_recording(
|
||||
cam_id, path, _parse_started_at(path), duration, size
|
||||
)
|
||||
added += 1
|
||||
|
||||
# уборка пропавших файлов
|
||||
for path in known:
|
||||
|
||||
Reference in New Issue
Block a user