feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
"""Резервные копии базы.
|
|
|
|
|
|
|
|
|
|
|
|
ТЗ требует копирования не реже раза в сутки. Копия — это `pg_dump`, и брать
|
|
|
|
|
|
его неоткуда, кроме самой базы: на машине с нативным бэкендом клиента Postgres
|
|
|
|
|
|
может не быть вовсе. Поэтому сначала пробуем локальный `pg_dump`, потом —
|
|
|
|
|
|
тот, что лежит в контейнере базы, и только если нет ни того ни другого,
|
|
|
|
|
|
честно говорим, чего не хватает.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
import base64
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
import logging
|
|
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import subprocess
|
2026-09-24 01:10:49 +03:00
|
|
|
|
import tempfile
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
from datetime import datetime, timezone
|
2026-09-24 01:10:49 +03:00
|
|
|
|
from getpass import getuser
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
from pathlib import Path
|
2026-09-24 01:10:49 +03:00
|
|
|
|
from urllib.parse import unquote, urlsplit
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
|
|
|
|
|
|
from app.config import get_settings
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
DIR = Path(__file__).resolve().parents[2] / "backups"
|
|
|
|
|
|
#: Контейнер базы в docker compose проекта.
|
|
|
|
|
|
CONTAINER = os.environ.get("POSTGRES_CONTAINER", "lct-hack-postgres-1")
|
|
|
|
|
|
TIMEOUT_S = 120
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# pg_dump 17 добавляет эту настройку в plain SQL, а целевая PostgreSQL 16 из
|
|
|
|
|
|
# Compose её ещё не знает. Остальной plain dump обратно совместим; удаляем
|
|
|
|
|
|
# только служебный SET, не данные и не DDL.
|
|
|
|
|
|
INCOMPATIBLE_SETTINGS = (b"SET transaction_timeout = 0;\n",)
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BackupError(RuntimeError):
|
|
|
|
|
|
"""Копию снять нечем или не удалось. Сообщение показывается администратору."""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
def _restrict_file_permissions(fd: int, path: Path, platform_name: str | None = None) -> None:
|
|
|
|
|
|
"""Закрыть резервную копию от остальных локальных пользователей.
|
|
|
|
|
|
|
|
|
|
|
|
chmod(0o600) не задаёт ACL на Windows, поэтому там icacls удаляет
|
|
|
|
|
|
наследуемые разрешения и оставляет полный доступ владельцу процесса и
|
|
|
|
|
|
системной учётной записи. При ошибке копию не публикуем.
|
|
|
|
|
|
"""
|
|
|
|
|
|
platform_name = platform_name or os.name
|
|
|
|
|
|
if platform_name == "nt":
|
|
|
|
|
|
username = getuser()
|
|
|
|
|
|
domain = os.environ.get("USERDOMAIN")
|
|
|
|
|
|
if domain and "\\" not in username:
|
|
|
|
|
|
username = f"{domain}\\{username}"
|
|
|
|
|
|
system_root = Path(os.environ.get("SystemRoot", "C:\\Windows"))
|
|
|
|
|
|
icacls = system_root / "System32" / "icacls.exe"
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
[str(icacls), str(path), "/inheritance:r", "/grant:r",
|
|
|
|
|
|
f"{username}:(F)", "*S-1-5-18:(F)"],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=15,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
|
|
|
|
raise BackupError("не удалось задать ACL резервной копии через icacls") from exc
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
|
raise BackupError("Windows не применила закрытый ACL к резервной копии")
|
|
|
|
|
|
return
|
|
|
|
|
|
if hasattr(os, "fchmod"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.fchmod(fd, 0o600)
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
raise BackupError("не удалось ограничить права резервной копии") from exc
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
raise BackupError("не удалось ограничить права резервной копии") from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
def _dsn() -> tuple[str, str, str, str, str]:
|
|
|
|
|
|
"""Разобрать DATABASE_URL на части для pg_dump."""
|
|
|
|
|
|
url = get_settings().database_url
|
2026-09-24 01:10:49 +03:00
|
|
|
|
try:
|
|
|
|
|
|
parsed = urlsplit(url)
|
|
|
|
|
|
user = unquote(parsed.username or "")
|
|
|
|
|
|
password = unquote(parsed.password or "")
|
|
|
|
|
|
host = parsed.hostname or ""
|
|
|
|
|
|
port = parsed.port
|
|
|
|
|
|
database = unquote(parsed.path.lstrip("/"))
|
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
|
# Не включать исходный URL в диагностику: он содержит пароль.
|
|
|
|
|
|
raise BackupError("не удалось разобрать DATABASE_URL") from exc
|
|
|
|
|
|
if not all((user, password, host, port, database)) or "/" in database:
|
|
|
|
|
|
raise BackupError("не удалось разобрать DATABASE_URL")
|
|
|
|
|
|
return user, password, host, str(port), database
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def listing() -> list[dict]:
|
|
|
|
|
|
"""Готовые копии, новые первыми."""
|
2026-09-24 01:10:49 +03:00
|
|
|
|
items = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
with os.scandir(DIR) as entries:
|
|
|
|
|
|
for entry in entries:
|
|
|
|
|
|
if not entry.name.endswith(".sql") or not entry.is_file(follow_symlinks=False):
|
|
|
|
|
|
continue
|
|
|
|
|
|
stat = entry.stat(follow_symlinks=False)
|
|
|
|
|
|
items.append({
|
|
|
|
|
|
"name": entry.name,
|
|
|
|
|
|
"size_bytes": stat.st_size,
|
|
|
|
|
|
"at": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
|
|
|
|
|
|
})
|
|
|
|
|
|
except FileNotFoundError:
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
return []
|
2026-09-24 01:10:49 +03:00
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
raise BackupError(f"не удалось прочитать каталог резервных копий: {exc}") from exc
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
return sorted(items, key=lambda item: item["at"], reverse=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create() -> dict:
|
|
|
|
|
|
"""Снять копию. Возвращает описание файла, как в `listing()`."""
|
|
|
|
|
|
user, password, host, port, database = _dsn()
|
2026-09-24 01:10:49 +03:00
|
|
|
|
try:
|
|
|
|
|
|
DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
raise BackupError(f"недоступен каталог резервных копий: {exc}") from exc
|
|
|
|
|
|
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
target = DIR / f"lct-{stamp}.sql"
|
|
|
|
|
|
|
|
|
|
|
|
if shutil.which("pg_dump"):
|
|
|
|
|
|
command = ["pg_dump", "-h", host, "-p", port, "-U", user, "-d", database]
|
|
|
|
|
|
env = {**os.environ, "PGPASSWORD": password}
|
2026-09-24 01:10:49 +03:00
|
|
|
|
stdin_data = None
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
elif shutil.which("docker"):
|
|
|
|
|
|
# В контейнере база доступна как localhost, а не по хосту из DSN.
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# Пароль передаём через stdin, а не аргументом `docker exec -e`,
|
|
|
|
|
|
# поскольку аргументы процесса могут быть видны другим пользователям.
|
|
|
|
|
|
script = (
|
|
|
|
|
|
"IFS= read -r encoded_password || exit 1; "
|
|
|
|
|
|
'PGPASSWORD="$(printf %s "$encoded_password" | base64 -d)" || exit 1; '
|
|
|
|
|
|
'export PGPASSWORD; exec pg_dump -U "$1" -d "$2"'
|
|
|
|
|
|
)
|
|
|
|
|
|
command = ["docker", "exec", "-i", CONTAINER, "sh", "-c", script,
|
|
|
|
|
|
"lct-pg-dump", user, database]
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
env = dict(os.environ)
|
2026-09-24 01:10:49 +03:00
|
|
|
|
stdin_data = base64.b64encode(password.encode("utf-8")) + b"\n"
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
else:
|
|
|
|
|
|
raise BackupError(
|
|
|
|
|
|
"нечем снять копию: нет ни pg_dump, ни docker. "
|
|
|
|
|
|
"Поставьте postgresql-client или запускайте базу в контейнере проекта"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-09-24 01:10:49 +03:00
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
command, capture_output=True, timeout=TIMEOUT_S, env=env, input=stdin_data
|
|
|
|
|
|
)
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
except subprocess.TimeoutExpired as exc:
|
|
|
|
|
|
raise BackupError(f"pg_dump не уложился в {TIMEOUT_S} с") from exc
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
raise BackupError(f"не запустился pg_dump: {exc}") from exc
|
|
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
|
raise BackupError(result.stderr.decode(errors="replace")[:300] or "pg_dump завершился с ошибкой")
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
dump = result.stdout
|
|
|
|
|
|
for setting in INCOMPATIBLE_SETTINGS:
|
|
|
|
|
|
dump = dump.replace(setting, b"")
|
|
|
|
|
|
if not dump.startswith(b"--\n-- PostgreSQL database dump"):
|
|
|
|
|
|
raise BackupError("pg_dump вернул неожиданный или пустой формат")
|
|
|
|
|
|
# Публикуем только полностью записанный dump. При аварийном завершении
|
|
|
|
|
|
# процесса список копий не должен показывать обрезанный файл как готовый.
|
|
|
|
|
|
try:
|
|
|
|
|
|
fd, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", dir=DIR)
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
raise BackupError(f"не удалось создать временный файл копии: {exc}") from exc
|
|
|
|
|
|
temporary = Path(temporary_name)
|
|
|
|
|
|
try:
|
|
|
|
|
|
with os.fdopen(fd, "wb") as stream:
|
|
|
|
|
|
_restrict_file_permissions(stream.fileno(), temporary)
|
|
|
|
|
|
stream.write(dump)
|
|
|
|
|
|
stream.flush()
|
|
|
|
|
|
os.fsync(stream.fileno())
|
|
|
|
|
|
temporary.replace(target)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
try:
|
|
|
|
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
log.exception("не удалось удалить временную копию %s", temporary.name)
|
|
|
|
|
|
if isinstance(exc, OSError):
|
|
|
|
|
|
raise BackupError(f"не удалось записать резервную копию: {exc}") from exc
|
|
|
|
|
|
raise
|
|
|
|
|
|
size_bytes = len(dump)
|
|
|
|
|
|
created_at = datetime.now(timezone.utc)
|
|
|
|
|
|
log.info("резервная копия: %s, %d КБ", target.name, size_bytes // 1024)
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
return {
|
|
|
|
|
|
"name": target.name,
|
2026-09-24 01:10:49 +03:00
|
|
|
|
"size_bytes": size_bytes,
|
|
|
|
|
|
"at": created_at,
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
}
|