chore: backup warning details
This commit is contained in:
@@ -699,12 +699,8 @@ async def device_topup_options_route(request: web.Request) -> web.Response:
|
||||
"currency": settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
||||
"title": f"+{count}",
|
||||
"subtitle": tariff.name(lang),
|
||||
"valid_from": _billing_iso_datetime(
|
||||
(rub_quote or stars_quote).get("valid_from")
|
||||
),
|
||||
"valid_until": _billing_iso_datetime(
|
||||
(rub_quote or stars_quote).get("valid_until")
|
||||
),
|
||||
"valid_from": _billing_iso_datetime((rub_quote or stars_quote).get("valid_from")),
|
||||
"valid_until": _billing_iso_datetime((rub_quote or stars_quote).get("valid_until")),
|
||||
"proration_ratio": float((rub_quote or stars_quote).get("proration_ratio") or 0),
|
||||
}
|
||||
if stars_quote and int(stars_quote.get("price") or 0) > 0:
|
||||
|
||||
@@ -182,13 +182,17 @@ def payment_units_for_activation(payment: Any, sale_mode: str) -> Any:
|
||||
"""Resolve purchased units from a payment record for webhook activation."""
|
||||
base = sale_mode_base(sale_mode)
|
||||
if sale_mode_is_traffic(base):
|
||||
return getattr(payment, "purchased_gb", None) or getattr(
|
||||
payment, "subscription_duration_months", None
|
||||
) or 1
|
||||
return (
|
||||
getattr(payment, "purchased_gb", None)
|
||||
or getattr(payment, "subscription_duration_months", None)
|
||||
or 1
|
||||
)
|
||||
if sale_mode_is_hwid_devices(base):
|
||||
return getattr(payment, "purchased_hwid_devices", None) or getattr(
|
||||
payment, "subscription_duration_months", None
|
||||
) or 1
|
||||
return (
|
||||
getattr(payment, "purchased_hwid_devices", None)
|
||||
or getattr(payment, "subscription_duration_months", None)
|
||||
or 1
|
||||
)
|
||||
return getattr(payment, "subscription_duration_months", None) or 1
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ BACKUP_RUNTIME_SETTING_KEYS = {
|
||||
"BACKUP_COMPOSE_SOURCE_DIR",
|
||||
"BACKUP_COMPOSE_EXCLUDE_DIRS",
|
||||
}
|
||||
TELEGRAM_DOCUMENT_CAPTION_LIMIT = 1024
|
||||
TELEGRAM_WARNING_DETAIL_LIMIT = 6
|
||||
TELEGRAM_WARNING_LINE_LIMIT = 220
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -263,16 +266,26 @@ class BackupWorker:
|
||||
def _stage_compose_source(self, target_dir: Path, warnings: list[str]) -> int:
|
||||
source_raw = (self.settings.BACKUP_COMPOSE_SOURCE_DIR or "").strip()
|
||||
if not source_raw:
|
||||
warnings.append("Compose source directory is not configured.")
|
||||
warnings.append(
|
||||
"Compose source directory is not configured. Set "
|
||||
"BACKUP_COMPOSE_SOURCE_DIR or mount the compose folder into the backup container."
|
||||
)
|
||||
return 0
|
||||
|
||||
source_dir = Path(source_raw).expanduser()
|
||||
if not source_dir.exists() or not source_dir.is_dir():
|
||||
warnings.append(f"Compose source directory is unavailable: {source_dir}")
|
||||
warnings.append(
|
||||
"If manual backup includes compose but scheduled backup does not, recreate "
|
||||
"the worker service with the compose-source mount. Compose source directory "
|
||||
f"is unavailable in this container: {source_dir}"
|
||||
)
|
||||
return 0
|
||||
|
||||
if not any((source_dir / marker).is_file() for marker in COMPOSE_MARKER_FILES):
|
||||
warnings.append(f"Compose source directory has no compose file marker: {source_dir}")
|
||||
warnings.append(
|
||||
"Check that COMPOSE_BACKUP_SOURCE points to the folder with docker-compose.yml. "
|
||||
f"Compose source directory has no compose file marker: {source_dir}"
|
||||
)
|
||||
|
||||
excluded_dirs = self._compose_excluded_dirs()
|
||||
files_count = 0
|
||||
@@ -358,8 +371,31 @@ class BackupWorker:
|
||||
f"Archive size: {self._human_size(result.size_bytes)}",
|
||||
]
|
||||
if result.warnings:
|
||||
lines.append(f"Warnings: {len(result.warnings)}")
|
||||
return "\n".join(lines)
|
||||
lines.append(f"Warnings ({len(result.warnings)}):")
|
||||
for index, warning in enumerate(
|
||||
result.warnings[:TELEGRAM_WARNING_DETAIL_LIMIT],
|
||||
start=1,
|
||||
):
|
||||
lines.append(f"{index}. {self._caption_warning(warning)}")
|
||||
hidden_count = len(result.warnings) - TELEGRAM_WARNING_DETAIL_LIMIT
|
||||
if hidden_count > 0:
|
||||
lines.append(f"... and {hidden_count} more warning(s)")
|
||||
return self._fit_caption(lines)
|
||||
|
||||
@staticmethod
|
||||
def _caption_warning(warning: str) -> str:
|
||||
text = " ".join(str(warning or "").split())
|
||||
if len(text) <= TELEGRAM_WARNING_LINE_LIMIT:
|
||||
return text
|
||||
return f"{text[: TELEGRAM_WARNING_LINE_LIMIT - 1].rstrip()}..."
|
||||
|
||||
@staticmethod
|
||||
def _fit_caption(lines: list[str]) -> str:
|
||||
caption = "\n".join(lines)
|
||||
if len(caption) <= TELEGRAM_DOCUMENT_CAPTION_LIMIT:
|
||||
return caption
|
||||
suffix = "\n... caption truncated"
|
||||
return f"{caption[: TELEGRAM_DOCUMENT_CAPTION_LIMIT - len(suffix)].rstrip()}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def _human_size(size_bytes: int) -> str:
|
||||
|
||||
@@ -54,6 +54,13 @@ BACKUP_THREAD_ID=123
|
||||
|
||||
`COMPOSE_BACKUP_SOURCE=.` означает папку рядом с текущим `docker-compose.yml`. Если compose лежит в другом месте, укажите абсолютный host-путь.
|
||||
|
||||
Ручное создание бэкапа из админки выполняется в `backend`-контейнере, а автоматический backup по расписанию - в `worker`-контейнере. Оба контейнера должны видеть `/app/compose-source`. Если ручной backup содержит compose-папку, а автоматический нет, пересоздайте worker после обновления compose:
|
||||
|
||||
```bash
|
||||
docker compose up -d --force-recreate worker
|
||||
docker compose exec worker ls -la /app/compose-source
|
||||
```
|
||||
|
||||
Если нужно запретить восстановление compose-файлов из контейнера, задайте:
|
||||
|
||||
```env
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
@@ -80,6 +81,7 @@ def test_backup_worker_creates_archive_with_db_dump_and_compose_snapshot(tmp_pat
|
||||
send_kwargs = bot.send_document.await_args.kwargs
|
||||
assert send_kwargs["chat_id"] == 123
|
||||
assert "Database dump: yes" in send_kwargs["caption"]
|
||||
assert "Warnings" not in send_kwargs["caption"]
|
||||
|
||||
with zipfile.ZipFile(result.archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
@@ -168,12 +170,50 @@ def test_backup_worker_does_not_fail_when_compose_source_is_not_mounted(tmp_path
|
||||
assert result.archive_path.is_file()
|
||||
assert result.db_dump_included is True
|
||||
assert result.compose_files_count == 0
|
||||
assert any("Compose source directory is unavailable" in item for item in result.warnings)
|
||||
assert any(
|
||||
"Compose source directory is unavailable in this container" in item
|
||||
for item in result.warnings
|
||||
)
|
||||
bot.send_document.assert_awaited_once()
|
||||
caption = bot.send_document.await_args.kwargs["caption"]
|
||||
assert "Warnings (1):" in caption
|
||||
assert "1. If manual backup includes compose but scheduled backup does not" in caption
|
||||
assert "Compose source directory is unavailable in this container" in caption
|
||||
assert "recreate the worker service" in caption
|
||||
with zipfile.ZipFile(result.archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
assert "database/shop.dump" in names
|
||||
|
||||
|
||||
def test_backup_worker_caption_lists_and_truncates_warning_details(tmp_path):
|
||||
settings = _settings(tmp_path, tmp_path / "compose")
|
||||
worker = _FakePgDumpBackupWorker(settings, _FakeBot())
|
||||
result = SimpleNamespace(
|
||||
completed_at=datetime.now(timezone.utc),
|
||||
db_dump_included=True,
|
||||
compose_files_count=0,
|
||||
size_bytes=1024,
|
||||
warnings=[
|
||||
"first warning",
|
||||
"second warning",
|
||||
"third warning",
|
||||
"fourth warning",
|
||||
"fifth warning",
|
||||
"sixth warning",
|
||||
"seventh warning",
|
||||
],
|
||||
)
|
||||
|
||||
caption = worker._caption(result)
|
||||
|
||||
assert "Warnings (7):" in caption
|
||||
assert "1. first warning" in caption
|
||||
assert "6. sixth warning" in caption
|
||||
assert "seventh warning" not in caption
|
||||
assert "... and 1 more warning(s)" in caption
|
||||
assert len(caption) <= 1024
|
||||
|
||||
|
||||
def test_backup_settings_refresh_restores_env_default_when_override_is_deleted(monkeypatch):
|
||||
from bot.services import settings_override_service
|
||||
|
||||
|
||||
Reference in New Issue
Block a user