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:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -41,3 +41,6 @@ aiofiles==23.2.1
|
||||
|
||||
# Вебхуки PayPalych (Flask)
|
||||
Flask==3.1.0
|
||||
|
||||
# Архивирование с паролем
|
||||
pyzipper==0.3.6
|
||||
|
||||
Reference in New Issue
Block a user