chore: change backup archive name style
This commit is contained in:
@@ -1,15 +1,20 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
BACKUP_APP_ID = "remnawave-minishop"
|
BACKUP_APP_ID = "remnawave-minishop"
|
||||||
BACKUP_FILENAME_PREFIX = "remnawave-minishop-backup-"
|
BACKUP_FILENAME_PREFIX = "minishop-"
|
||||||
BACKUP_FORMAT_VERSION = 1
|
BACKUP_FORMAT_VERSION = 1
|
||||||
BACKUP_MANIFEST_NAME = "manifest.json"
|
BACKUP_MANIFEST_NAME = "manifest.json"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_filename_timestamp() -> str:
|
||||||
|
return datetime.now().astimezone().strftime("%Y%m%d-%H-%M")
|
||||||
|
|
||||||
|
|
||||||
def file_sha256(path: Path) -> str:
|
def file_sha256(path: Path) -> str:
|
||||||
digest = hashlib.sha256()
|
digest = hashlib.sha256()
|
||||||
with path.open("rb") as handle:
|
with path.open("rb") as handle:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from bot.services.backup_archive import (
|
|||||||
BACKUP_FORMAT_VERSION,
|
BACKUP_FORMAT_VERSION,
|
||||||
BACKUP_MANIFEST_NAME,
|
BACKUP_MANIFEST_NAME,
|
||||||
attach_archive_integrity,
|
attach_archive_integrity,
|
||||||
|
backup_filename_timestamp,
|
||||||
build_file_records,
|
build_file_records,
|
||||||
write_manifest,
|
write_manifest,
|
||||||
write_zip_from_directory,
|
write_zip_from_directory,
|
||||||
@@ -39,7 +40,7 @@ BACKUP_MAX_COMPOSE_BYTES = 1024 * 1024 * 1024
|
|||||||
BACKUP_MAX_COMPOSE_MEMBER_BYTES = 256 * 1024 * 1024
|
BACKUP_MAX_COMPOSE_MEMBER_BYTES = 256 * 1024 * 1024
|
||||||
BACKUP_MAX_COMPRESSION_RATIO = 200
|
BACKUP_MAX_COMPRESSION_RATIO = 200
|
||||||
BACKUP_ZIP_BOMB_MIN_BYTES = 100 * 1024 * 1024
|
BACKUP_ZIP_BOMB_MIN_BYTES = 100 * 1024 * 1024
|
||||||
COMPOSE_PRE_RESTORE_PREFIX = "remnawave-minishop-compose-pre-restore-"
|
COMPOSE_PRE_RESTORE_PREFIX = "minishop-pre-restore-"
|
||||||
SAFE_ARCHIVE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.@+-]{0,220}\.zip$")
|
SAFE_ARCHIVE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.@+-]{0,220}\.zip$")
|
||||||
|
|
||||||
|
|
||||||
@@ -183,9 +184,8 @@ class BackupRestoreService:
|
|||||||
) -> BackupArchiveInfo:
|
) -> BackupArchiveInfo:
|
||||||
self._validate_archive_for_restore(temp_path)
|
self._validate_archive_for_restore(temp_path)
|
||||||
digest = self._file_digest(temp_path)
|
digest = self._file_digest(temp_path)
|
||||||
stamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S%z")
|
stamp = backup_filename_timestamp()
|
||||||
safe_original = self._safe_original_stem(original_filename)
|
archive_name = f"{BACKUP_FILENAME_PREFIX}uploaded-{stamp}-{digest}.zip"
|
||||||
archive_name = f"{BACKUP_FILENAME_PREFIX}uploaded-{stamp}-{digest}-{safe_original}.zip"
|
|
||||||
target_path = self._unique_archive_path(archive_name)
|
target_path = self._unique_archive_path(archive_name)
|
||||||
temp_path.replace(target_path)
|
temp_path.replace(target_path)
|
||||||
return self.inspect_archive(target_path)
|
return self.inspect_archive(target_path)
|
||||||
@@ -346,12 +346,12 @@ class BackupRestoreService:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
def _snapshot_current_compose(self, target_dir: Path) -> Optional[Path]:
|
def _snapshot_current_compose(self, target_dir: Path) -> Optional[Path]:
|
||||||
stamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S%z")
|
stamp = backup_filename_timestamp()
|
||||||
archive_path = self.backup_dir() / f"{COMPOSE_PRE_RESTORE_PREFIX}{stamp}.zip"
|
archive_path = self._unique_archive_path(f"{COMPOSE_PRE_RESTORE_PREFIX}{stamp}.zip")
|
||||||
excluded_dirs = self._compose_excluded_dirs()
|
excluded_dirs = self._compose_excluded_dirs()
|
||||||
files_count = 0
|
files_count = 0
|
||||||
with tempfile.TemporaryDirectory(
|
with tempfile.TemporaryDirectory(
|
||||||
prefix=f"{COMPOSE_PRE_RESTORE_PREFIX}{stamp}-",
|
prefix=f"{archive_path.stem}-",
|
||||||
dir=self.backup_dir(),
|
dir=self.backup_dir(),
|
||||||
) as tmp:
|
) as tmp:
|
||||||
staging_dir = Path(tmp)
|
staging_dir = Path(tmp)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from bot.services.backup_archive import (
|
|||||||
BACKUP_FILENAME_PREFIX,
|
BACKUP_FILENAME_PREFIX,
|
||||||
BACKUP_FORMAT_VERSION,
|
BACKUP_FORMAT_VERSION,
|
||||||
attach_archive_integrity,
|
attach_archive_integrity,
|
||||||
|
backup_filename_timestamp,
|
||||||
build_file_records,
|
build_file_records,
|
||||||
write_manifest,
|
write_manifest,
|
||||||
write_zip_from_directory,
|
write_zip_from_directory,
|
||||||
@@ -145,7 +146,7 @@ class BackupWorker:
|
|||||||
|
|
||||||
async def create_backup(self, *, backup_type: str = "scheduled") -> BackupResult:
|
async def create_backup(self, *, backup_type: str = "scheduled") -> BackupResult:
|
||||||
started_at = datetime.now(timezone.utc)
|
started_at = datetime.now(timezone.utc)
|
||||||
stamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S%z")
|
stamp = backup_filename_timestamp()
|
||||||
archive_name = f"{BACKUP_FILENAME_PREFIX}{stamp}.zip"
|
archive_name = f"{BACKUP_FILENAME_PREFIX}{stamp}.zip"
|
||||||
backup_dir = Path(self.settings.BACKUP_DIR).expanduser()
|
backup_dir = Path(self.settings.BACKUP_DIR).expanduser()
|
||||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Minishop умеет автоматически собирать ZIP-бэкапы
|
|||||||
Типовой файл называется так:
|
Типовой файл называется так:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
remnawave-minishop-backup-20260527-120000+0300.zip
|
minishop-20260527-12-00.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
Внутри:
|
Внутри:
|
||||||
@@ -86,7 +86,7 @@ COMPOSE_RESTORE_MODE=ro
|
|||||||
Compose-файлы восстанавливаются поверх текущей папки. Перед заменой backend создает pre-restore snapshot текущего compose-каталога рядом с остальными архивами:
|
Compose-файлы восстанавливаются поверх текущей папки. Перед заменой backend создает pre-restore snapshot текущего compose-каталога рядом с остальными архивами:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
remnawave-minishop-compose-pre-restore-YYYYMMDD-HHMMSS+ZZZZ.zip
|
minishop-pre-restore-YYYYMMDD-HH-MM.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
После восстановления compose-папки перезапустите нужные сервисы, чтобы изменения `docker-compose.yml`, `.env`, Caddyfile/Nginx-конфигов и других файлов реально применились:
|
После восстановления compose-папки перезапустите нужные сервисы, чтобы изменения `docker-compose.yml`, `.env`, Caddyfile/Nginx-конфигов и других файлов реально применились:
|
||||||
@@ -131,7 +131,7 @@ Backend валидирует архив до восстановления:
|
|||||||
Если админка недоступна, можно восстановить дамп вручную:
|
Если админка недоступна, можно восстановить дамп вручную:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
unzip remnawave-minishop-backup-YYYYMMDD-HHMMSS+ZZZZ.zip -d restore
|
unzip minishop-YYYYMMDD-HH-MM.zip -d restore
|
||||||
docker compose cp restore/database/remnawave_minishop.dump postgres:/tmp/remnawave_minishop.dump
|
docker compose cp restore/database/remnawave_minishop.dump postgres:/tmp/remnawave_minishop.dump
|
||||||
docker compose stop backend worker
|
docker compose stop backend worker
|
||||||
docker compose exec postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges /tmp/remnawave_minishop.dump'
|
docker compose exec postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges /tmp/remnawave_minishop.dump'
|
||||||
|
|||||||
@@ -270,9 +270,17 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
})();
|
})();
|
||||||
|
const compactBackupStamp = (date) => {
|
||||||
|
const pad = (value) => String(value).padStart(2, "0");
|
||||||
|
return [
|
||||||
|
`${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`,
|
||||||
|
pad(date.getHours()),
|
||||||
|
pad(date.getMinutes()),
|
||||||
|
].join("-");
|
||||||
|
};
|
||||||
const mockBackups = [
|
const mockBackups = [
|
||||||
{
|
{
|
||||||
name: "remnawave-minishop-backup-20260527-120000+0300.zip",
|
name: "minishop-20260527-12-00.zip",
|
||||||
size_bytes: 184320,
|
size_bytes: 184320,
|
||||||
modified_at: "2026-05-27T09:00:00Z",
|
modified_at: "2026-05-27T09:00:00Z",
|
||||||
created_at: "2026-05-27T09:00:00Z",
|
created_at: "2026-05-27T09:00:00Z",
|
||||||
@@ -285,7 +293,7 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
manifest: {},
|
manifest: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "remnawave-minishop-backup-20260527-110000+0300.zip",
|
name: "minishop-20260527-11-00.zip",
|
||||||
size_bytes: 153600,
|
size_bytes: 153600,
|
||||||
modified_at: "2026-05-27T08:00:00Z",
|
modified_at: "2026-05-27T08:00:00Z",
|
||||||
created_at: "2026-05-27T08:00:00Z",
|
created_at: "2026-05-27T08:00:00Z",
|
||||||
@@ -477,14 +485,9 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
}
|
}
|
||||||
if (path === "/admin/backups/create") {
|
if (path === "/admin/backups/create") {
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const stamp = createdAt
|
|
||||||
.toISOString()
|
|
||||||
.replace(/[-:]/g, "")
|
|
||||||
.replace("T", "-")
|
|
||||||
.replace(/\.\d{3}Z$/, "+0000");
|
|
||||||
const archive = {
|
const archive = {
|
||||||
...mockBackups[0],
|
...mockBackups[0],
|
||||||
name: `remnawave-minishop-backup-${stamp}.zip`,
|
name: `minishop-${compactBackupStamp(createdAt)}.zip`,
|
||||||
modified_at: createdAt.toISOString(),
|
modified_at: createdAt.toISOString(),
|
||||||
created_at: createdAt.toISOString(),
|
created_at: createdAt.toISOString(),
|
||||||
created_at_local: createdAt.toISOString(),
|
created_at_local: createdAt.toISOString(),
|
||||||
@@ -505,14 +508,15 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (path === "/admin/backups/upload") {
|
if (path === "/admin/backups/upload") {
|
||||||
|
const uploadedAt = new Date();
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
archive: {
|
archive: {
|
||||||
...mockBackups[0],
|
...mockBackups[0],
|
||||||
name: `remnawave-minishop-backup-uploaded-${Date.now()}.zip`,
|
name: `minishop-uploaded-${compactBackupStamp(uploadedAt)}-0000000000000000.zip`,
|
||||||
modified_at: new Date().toISOString(),
|
modified_at: uploadedAt.toISOString(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: uploadedAt.toISOString(),
|
||||||
created_at_local: new Date().toISOString(),
|
created_at_local: uploadedAt.toISOString(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -526,8 +530,7 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
database_restored: true,
|
database_restored: true,
|
||||||
compose_files_restored: 6,
|
compose_files_restored: 6,
|
||||||
compose_target_dir: "/app/compose-source",
|
compose_target_dir: "/app/compose-source",
|
||||||
compose_pre_restore_archive:
|
compose_pre_restore_archive: "data/backups/minishop-pre-restore-20260527-12-15.zip",
|
||||||
"data/backups/remnawave-minishop-compose-pre-restore-20260527-121500+0300.zip",
|
|
||||||
warnings: [],
|
warnings: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,6 +13,7 @@ from bot.services.backup_archive import (
|
|||||||
write_zip_from_directory,
|
write_zip_from_directory,
|
||||||
)
|
)
|
||||||
from bot.services.backup_restore_service import (
|
from bot.services.backup_restore_service import (
|
||||||
|
COMPOSE_PRE_RESTORE_PREFIX,
|
||||||
BackupArchiveError,
|
BackupArchiveError,
|
||||||
BackupRestoreService,
|
BackupRestoreService,
|
||||||
)
|
)
|
||||||
@@ -77,7 +79,7 @@ def test_backup_restore_service_lists_archives_with_contents(tmp_path):
|
|||||||
compose_dir = tmp_path / "compose"
|
compose_dir = tmp_path / "compose"
|
||||||
compose_dir.mkdir()
|
compose_dir.mkdir()
|
||||||
settings = _settings(tmp_path, compose_dir)
|
settings = _settings(tmp_path, compose_dir)
|
||||||
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-120000+0300.zip"
|
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-12-00.zip"
|
||||||
_write_backup_archive(archive_path)
|
_write_backup_archive(archive_path)
|
||||||
|
|
||||||
archives = BackupRestoreService(settings).list_archives()
|
archives = BackupRestoreService(settings).list_archives()
|
||||||
@@ -97,13 +99,22 @@ def test_backup_restore_service_rejects_path_traversal_archive_name(tmp_path):
|
|||||||
service.archive_path_for_name("../backup.zip")
|
service.archive_path_for_name("../backup.zip")
|
||||||
|
|
||||||
|
|
||||||
def test_backup_restore_service_restores_compose_and_snapshots_current(tmp_path):
|
def test_backup_restore_service_restores_compose_and_snapshots_current(tmp_path, monkeypatch):
|
||||||
compose_dir = tmp_path / "compose"
|
compose_dir = tmp_path / "compose"
|
||||||
compose_dir.mkdir()
|
compose_dir.mkdir()
|
||||||
(compose_dir / "docker-compose.yml").write_text("old: true\n", encoding="utf-8")
|
(compose_dir / "docker-compose.yml").write_text("old: true\n", encoding="utf-8")
|
||||||
|
|
||||||
settings = _settings(tmp_path, compose_dir)
|
settings = _settings(tmp_path, compose_dir)
|
||||||
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-120000+0300.zip"
|
monkeypatch.setattr(
|
||||||
|
"bot.services.backup_restore_service.backup_filename_timestamp",
|
||||||
|
lambda: "20260527-12-15",
|
||||||
|
)
|
||||||
|
existing_snapshot = (
|
||||||
|
Path(settings.BACKUP_DIR) / f"{COMPOSE_PRE_RESTORE_PREFIX}20260527-12-15.zip"
|
||||||
|
)
|
||||||
|
existing_snapshot.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
existing_snapshot.write_text("existing", encoding="utf-8")
|
||||||
|
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-12-00.zip"
|
||||||
_write_backup_archive(archive_path, include_db=False)
|
_write_backup_archive(archive_path, include_db=False)
|
||||||
|
|
||||||
service = BackupRestoreService(settings)
|
service = BackupRestoreService(settings)
|
||||||
@@ -118,7 +129,10 @@ def test_backup_restore_service_restores_compose_and_snapshots_current(tmp_path)
|
|||||||
assert (compose_dir / "docker-compose.yml").read_text(encoding="utf-8") == "services: {}\n"
|
assert (compose_dir / "docker-compose.yml").read_text(encoding="utf-8") == "services: {}\n"
|
||||||
assert (compose_dir / ".env").read_text(encoding="utf-8") == "POSTGRES_PASSWORD=secret\n"
|
assert (compose_dir / ".env").read_text(encoding="utf-8") == "POSTGRES_PASSWORD=secret\n"
|
||||||
assert result.compose_pre_restore_archive
|
assert result.compose_pre_restore_archive
|
||||||
assert Path(result.compose_pre_restore_archive).is_file()
|
snapshot_path = Path(result.compose_pre_restore_archive)
|
||||||
|
assert snapshot_path.is_file()
|
||||||
|
assert snapshot_path.name == f"{COMPOSE_PRE_RESTORE_PREFIX}20260527-12-15-2.zip"
|
||||||
|
assert existing_snapshot.read_text(encoding="utf-8") == "existing"
|
||||||
snapshot = service.inspect_archive(Path(result.compose_pre_restore_archive))
|
snapshot = service.inspect_archive(Path(result.compose_pre_restore_archive))
|
||||||
assert snapshot.has_compose is True
|
assert snapshot.has_compose is True
|
||||||
assert snapshot.compose_files_count == 1
|
assert snapshot.compose_files_count == 1
|
||||||
@@ -128,7 +142,7 @@ def test_backup_restore_service_prevents_zip_slip_in_compose_restore(tmp_path):
|
|||||||
compose_dir = tmp_path / "compose"
|
compose_dir = tmp_path / "compose"
|
||||||
compose_dir.mkdir()
|
compose_dir.mkdir()
|
||||||
settings = _settings(tmp_path, compose_dir)
|
settings = _settings(tmp_path, compose_dir)
|
||||||
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-120000+0300.zip"
|
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-12-00.zip"
|
||||||
_write_backup_archive(archive_path, include_db=False, unsafe=True)
|
_write_backup_archive(archive_path, include_db=False, unsafe=True)
|
||||||
|
|
||||||
with pytest.raises(BackupArchiveError):
|
with pytest.raises(BackupArchiveError):
|
||||||
@@ -145,7 +159,7 @@ def test_backup_restore_service_runs_pg_restore_for_dump(tmp_path):
|
|||||||
compose_dir = tmp_path / "compose"
|
compose_dir = tmp_path / "compose"
|
||||||
compose_dir.mkdir()
|
compose_dir.mkdir()
|
||||||
settings = _settings(tmp_path, compose_dir)
|
settings = _settings(tmp_path, compose_dir)
|
||||||
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-120000+0300.zip"
|
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-12-00.zip"
|
||||||
_write_backup_archive(archive_path, include_compose=False)
|
_write_backup_archive(archive_path, include_compose=False)
|
||||||
service = BackupRestoreService(settings)
|
service = BackupRestoreService(settings)
|
||||||
restored_payloads = []
|
restored_payloads = []
|
||||||
@@ -172,7 +186,7 @@ def test_backup_restore_service_accepts_archive_from_another_instance(tmp_path):
|
|||||||
compose_dir.mkdir()
|
compose_dir.mkdir()
|
||||||
target_settings = _settings(tmp_path, compose_dir, BOT_TOKEN="target-token")
|
target_settings = _settings(tmp_path, compose_dir, BOT_TOKEN="target-token")
|
||||||
archive_path = Path(target_settings.BACKUP_DIR) / (
|
archive_path = Path(target_settings.BACKUP_DIR) / (
|
||||||
f"{BACKUP_FILENAME_PREFIX}20260527-120000+0300.zip"
|
f"{BACKUP_FILENAME_PREFIX}20260527-12-00.zip"
|
||||||
)
|
)
|
||||||
_write_backup_archive(archive_path, include_compose=False)
|
_write_backup_archive(archive_path, include_compose=False)
|
||||||
|
|
||||||
@@ -205,11 +219,29 @@ def test_backup_restore_service_validates_uploaded_zip(tmp_path):
|
|||||||
BackupRestoreService(settings).import_uploaded_archive(temp_path, "backup.zip")
|
BackupRestoreService(settings).import_uploaded_archive(temp_path, "backup.zip")
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_restore_service_imports_uploaded_archive_with_compact_name(tmp_path):
|
||||||
|
compose_dir = tmp_path / "compose"
|
||||||
|
compose_dir.mkdir()
|
||||||
|
settings = _settings(tmp_path, compose_dir)
|
||||||
|
temp_path = tmp_path / "source.zip"
|
||||||
|
_write_backup_archive(temp_path)
|
||||||
|
|
||||||
|
archive = BackupRestoreService(settings).import_uploaded_archive(
|
||||||
|
temp_path,
|
||||||
|
"very-long-original-backup-name.zip",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert re.fullmatch(
|
||||||
|
r"minishop-uploaded-\d{8}-\d{2}-\d{2}-[a-f0-9]{16}\.zip",
|
||||||
|
archive.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_backup_restore_service_rejects_tampered_archive(tmp_path):
|
def test_backup_restore_service_rejects_tampered_archive(tmp_path):
|
||||||
compose_dir = tmp_path / "compose"
|
compose_dir = tmp_path / "compose"
|
||||||
compose_dir.mkdir()
|
compose_dir.mkdir()
|
||||||
settings = _settings(tmp_path, compose_dir)
|
settings = _settings(tmp_path, compose_dir)
|
||||||
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-120000+0300.zip"
|
archive_path = Path(settings.BACKUP_DIR) / f"{BACKUP_FILENAME_PREFIX}20260527-12-00.zip"
|
||||||
_write_backup_archive(archive_path, include_compose=False)
|
_write_backup_archive(archive_path, include_compose=False)
|
||||||
|
|
||||||
tampered_path = archive_path.with_name("tampered.zip")
|
tampered_path = archive_path.with_name("tampered.zip")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -76,6 +77,7 @@ def test_backup_worker_creates_archive_with_db_dump_and_compose_snapshot(tmp_pat
|
|||||||
assert result.archive_path.is_file()
|
assert result.archive_path.is_file()
|
||||||
assert result.db_dump_included is True
|
assert result.db_dump_included is True
|
||||||
assert result.compose_files_count == 3
|
assert result.compose_files_count == 3
|
||||||
|
assert re.fullmatch(r"minishop-\d{8}-\d{2}-\d{2}\.zip", result.archive_path.name)
|
||||||
assert not old_archive.exists()
|
assert not old_archive.exists()
|
||||||
bot.send_document.assert_awaited_once()
|
bot.send_document.assert_awaited_once()
|
||||||
send_kwargs = bot.send_document.await_args.kwargs
|
send_kwargs = bot.send_document.await_args.kwargs
|
||||||
@@ -146,12 +148,12 @@ def test_backup_worker_can_create_manual_backup(tmp_path):
|
|||||||
def test_backup_worker_allocates_unique_archive_path(tmp_path):
|
def test_backup_worker_allocates_unique_archive_path(tmp_path):
|
||||||
settings = _settings(tmp_path, tmp_path / "compose")
|
settings = _settings(tmp_path, tmp_path / "compose")
|
||||||
worker = _FakePgDumpBackupWorker(settings, _FakeBot())
|
worker = _FakePgDumpBackupWorker(settings, _FakeBot())
|
||||||
archive_path = tmp_path / "remnawave-minishop-backup-20260527-120000+0300.zip"
|
archive_path = tmp_path / "minishop-20260527-12-00.zip"
|
||||||
archive_path.write_text("existing", encoding="utf-8")
|
archive_path.write_text("existing", encoding="utf-8")
|
||||||
|
|
||||||
unique_path = worker._unique_archive_path(archive_path)
|
unique_path = worker._unique_archive_path(archive_path)
|
||||||
|
|
||||||
assert unique_path.name == "remnawave-minishop-backup-20260527-120000+0300-2.zip"
|
assert unique_path.name == "minishop-20260527-12-00-2.zip"
|
||||||
|
|
||||||
|
|
||||||
def test_backup_worker_does_not_fail_when_compose_source_is_not_mounted(tmp_path):
|
def test_backup_worker_does_not_fail_when_compose_source_is_not_mounted(tmp_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user