Add support for password-protected backup archives

- Updated .env.example to include BACKUP_ARCHIVE_PASSWORD variable.
- Added pyzipper to requirements.txt for ZIP file encryption.
- Modified Settings class in config.py to handle BACKUP_ARCHIVE_PASSWORD.
- Enhanced BackupService to create and send password-protected ZIP archives if a password is provided.
This commit is contained in:
PEDZEO
2025-12-27 03:58:48 +03:00
parent 04e83814b4
commit 34db5a0d28
4 changed files with 62 additions and 5 deletions
+2
View File
@@ -485,6 +485,8 @@ BACKUP_SEND_ENABLED=true
BACKUP_SEND_CHAT_ID=-100123456789 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА!
# ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
BACKUP_SEND_TOPIC_ID=123 # Опционально: ID топика
# Пароль для архива бекапа (опционально). Если задан - бекап отправляется в зашифрованном ZIP с AES
BACKUP_ARCHIVE_PASSWORD=
# ===== ПРОВЕРКА ОБНОВЛЕНИЙ БОТА =====
VERSION_CHECK_ENABLED=true
+5
View File
@@ -439,6 +439,7 @@ class Settings(BaseSettings):
BACKUP_SEND_ENABLED: bool = False
BACKUP_SEND_CHAT_ID: Optional[str] = None
BACKUP_SEND_TOPIC_ID: Optional[int] = None
BACKUP_ARCHIVE_PASSWORD: Optional[str] = None
EXTERNAL_ADMIN_TOKEN: Optional[str] = None
EXTERNAL_ADMIN_TOKEN_BOT_ID: Optional[int] = None
@@ -1449,6 +1450,10 @@ class Settings(BaseSettings):
return (self.BACKUP_SEND_ENABLED and
self.get_backup_send_chat_id() is not None)
def get_backup_archive_password(self) -> Optional[str]:
password = (self.BACKUP_ARCHIVE_PASSWORD or "").strip()
return password if password else None
def get_referral_settings(self) -> Dict:
return {
"program_enabled": self.is_referral_program_enabled(),
+52 -5
View File
@@ -12,6 +12,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import aiofiles
import pyzipper
from aiogram.types import FSInputFile
from sqlalchemy import inspect, select, text
from sqlalchemy.ext.asyncio import AsyncSession
@@ -1534,13 +1535,24 @@ class BackupService:
if not chat_id:
return
password = settings.get_backup_archive_password()
file_to_send = file_path
temp_zip_path = None
if password:
temp_zip_path = await self._create_password_protected_archive(file_path, password)
if temp_zip_path:
file_to_send = temp_zip_path
caption = f"📦 <b>Резервная копия</b>\n\n"
if password:
caption += f"🔐 <b>Архив защищён паролем</b>\n\n"
caption += f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"
send_kwargs = {
'chat_id': chat_id,
'document': FSInputFile(file_path),
'caption': (
f"📦 <b>Резервная копия</b>\n\n"
f"⏰ <i>{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"
),
'document': FSInputFile(file_to_send),
'caption': caption,
'parse_mode': 'HTML'
}
@@ -1549,8 +1561,43 @@ class BackupService:
await self.bot.send_document(**send_kwargs)
logger.info(f"Бекап отправлен в чат {chat_id}")
if temp_zip_path and Path(temp_zip_path).exists():
try:
Path(temp_zip_path).unlink()
except Exception as cleanup_error:
logger.warning(f"Не удалось удалить временный архив: {cleanup_error}")
except Exception as e:
logger.error(f"Ошибка отправки бекапа в чат: {e}")
async def _create_password_protected_archive(self, file_path: str, password: str) -> Optional[str]:
try:
source_path = Path(file_path)
if not source_path.exists():
logger.error(f"Исходный файл бекапа не найден: {file_path}")
return None
zip_filename = source_path.stem + ".zip"
zip_path = source_path.parent / zip_filename
def create_zip():
with pyzipper.AESZipFile(
zip_path,
'w',
compression=pyzipper.ZIP_DEFLATED,
encryption=pyzipper.WZ_AES
) as zf:
zf.setpassword(password.encode('utf-8'))
zf.write(source_path, arcname=source_path.name)
await asyncio.to_thread(create_zip)
logger.info(f"Создан защищённый паролем архив: {zip_path}")
return str(zip_path)
except Exception as e:
logger.error(f"Ошибка создания защищённого архива: {e}")
return None
backup_service = BackupService()
+3
View File
@@ -41,3 +41,6 @@ aiofiles==23.2.1
# Вебхуки PayPalych (Flask)
Flask==3.1.0
# Архивирование с паролем
pyzipper==0.3.6