lct-hack/backend/scripts/backup.py

42 lines
1.4 KiB
Python
Raw Normal View History

feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24) Целый раздел ТЗ, от которого не было ни одной функции. - Учётные записи: завести, сменить роль и службу, заблокировать, сбросить пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль и проверка «это твой разбор». - Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия классификатора, число живых занятий. Отдельной строкой — секрет сессии: значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть её должен администратор, а не проверяющий на защите. - Журнал действий с фильтром, только на чтение. - Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный, а если его нет — из контейнера базы; если нет ни того ни другого, администратор видит, чего не хватает, а кнопка не молчит. Границы роли выражены отсутствием маршрутов, а не проверками внутри них: в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста проверяют это перебором маршрутов роутера — так запрет нельзя ослабить, случайно добавив обработчик. Найдено по ходу: тесты с базой падали в компании и проходили поодиночке. Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура, дающая движок на тест. 193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
"""make backup: снять резервную копию базы.
Та же операция, что кнопка на АРМ администратора, — но вызываемая из cron:
ТЗ требует копирования не реже раза в сутки, а полагаться на то, что кто-то
нажмёт кнопку, нельзя.
make backup снять копию
0 3 * * * cd … && make backup в crontab демо-машины
"""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.admin.backup import BackupError, create, listing # noqa: E402
#: Сколько копий держать. Место на демо-машине не бесконечно, а копия за
#: позапрошлую неделю не нужна никому.
KEEP = 14
def main() -> int:
try:
created = create()
except BackupError as exc:
print(f"копия не снята: {exc}")
return 1
print(f"{created['name']}: {created['size_bytes'] // 1024} КБ")
old = listing()[KEEP:]
for item in old:
(ROOT / "backups" / item["name"]).unlink(missing_ok=True)
if old:
print(f"удалено старых копий: {len(old)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())