"""Фоновый пробер сетевых хранилищ.
Панель сама опрашивает примонтированные к её машине шарды (os.statvfs):
раз в GHARD_SHARE_INTERVAL секунд пишет точку в share_samples. Путь не
читается (отвалился NFS/SMB, сеть лежит) — пишется ok=0, панель покажет
«offline». Старше 7 дней точки удаляются (политика хранения — этап 5).
"""
import asyncio
import os
from datetime import datetime, timedelta, timezone
from app.config import get_settings
from app.db import get_db
RETENTION_DAYS = 7
def _probe(path: str) -> tuple[int, int, int]:
"""(total, used, ok) в байтах; недоступный путь → (0, 0, 0)."""
try:
st = os.statvfs(path)
except OSError:
return 0, 0, 0
total = st.f_blocks * st.f_frsize
free = st.f_bfree * st.f_frsize
return total, total - free, 1
async def sample_once() -> int:
"""Одна волна проб по всем шардам. Возвращает число точек."""
db = get_db()
cursor = await db.execute("SELECT id, path FROM shares")
rows = await cursor.fetchall()
if not rows:
return 0
now = datetime.now(timezone.utc).isoformat()
points = 0
for row in rows:
total, used, ok = _probe(row["path"])
await db.execute(
"INSERT INTO share_samples (share_id, ts, total, used, ok) VALUES (?, ?, ?, ?, ?)",
(row["id"], now, total, used, ok),
)
points += 1
# политика хранения: точки шардов старше 7 дней не нужны
cutoff = (datetime.now(timezone.utc) - timedelta(days=RETENTION_DAYS)).isoformat()
await db.execute("DELETE FROM share_samples WHERE ts < ?", (cutoff,))
await db.commit()
return points
async def loop() -> None:
interval = get_settings().share_interval
while True:
try:
await sample_once()
except Exception as exc: # не роняем цикл из-за одной ошибки
print(f"shares probe error: {exc}", flush=True)
await asyncio.sleep(interval)