159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
import os
|
|
import subprocess
|
|
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
|
|
from aiogram.types import BufferedInputFile
|
|
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
|
|
|
|
from bot import bot
|
|
from logger import logger
|
|
|
|
|
|
async def backup_database() -> Exception | None:
|
|
"""
|
|
Создает резервную копию базы данных и отправляет ее администраторам.
|
|
|
|
Returns:
|
|
Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении
|
|
"""
|
|
backup_file_path, exception = _create_database_backup()
|
|
|
|
if exception:
|
|
logger.error(f"Ошибка при создании бэкапа базы данных: {exception}")
|
|
return exception
|
|
|
|
try:
|
|
await _send_backup_to_admins(backup_file_path)
|
|
exception = _cleanup_old_backups()
|
|
|
|
if exception:
|
|
logger.error(f"Ошибка при удалении старых бэкапов базы данных: {exception}")
|
|
return exception
|
|
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при отправке бэкапа базы данных: {e}")
|
|
return e
|
|
|
|
|
|
def _create_database_backup() -> tuple[str | None, Exception | None]:
|
|
"""
|
|
Создает резервную копию базы данных PostgreSQL.
|
|
|
|
Returns:
|
|
Tuple[Optional[str], Optional[Exception]]: Путь к файлу бэкапа и исключение (если произошла ошибка)
|
|
"""
|
|
date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S")
|
|
|
|
backup_dir = Path(BACK_DIR)
|
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
filename = backup_dir / f"{DB_NAME}-backup-{date_formatted}.sql"
|
|
|
|
try:
|
|
os.environ["PGPASSWORD"] = DB_PASSWORD
|
|
|
|
subprocess.run(
|
|
[
|
|
"pg_dump",
|
|
"-U",
|
|
DB_USER,
|
|
"-h",
|
|
PG_HOST,
|
|
"-p",
|
|
PG_PORT,
|
|
"-F",
|
|
"c",
|
|
"-f",
|
|
str(filename),
|
|
DB_NAME,
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
logger.info(f"Бэкап базы данных создан: {filename}")
|
|
return str(filename), None
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Ошибка при выполнении pg_dump: {e.stderr}")
|
|
return None, e
|
|
except Exception as e:
|
|
logger.error(f"Непредвиденная ошибка при создании бэкапа: {e}")
|
|
return None, e
|
|
finally:
|
|
if "PGPASSWORD" in os.environ:
|
|
del os.environ["PGPASSWORD"]
|
|
|
|
|
|
def _cleanup_old_backups() -> Exception | None:
|
|
"""
|
|
Удаляет бэкапы старше 3 дней.
|
|
|
|
Returns:
|
|
Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении
|
|
"""
|
|
try:
|
|
backup_dir = Path(BACK_DIR)
|
|
if not backup_dir.exists():
|
|
return None
|
|
|
|
cutoff_date = datetime.now() - timedelta(days=3)
|
|
|
|
for backup_file in backup_dir.glob("*.sql"):
|
|
if backup_file.is_file():
|
|
file_mtime = datetime.fromtimestamp(backup_file.stat().st_mtime)
|
|
if file_mtime < cutoff_date:
|
|
backup_file.unlink()
|
|
logger.info(f"Удален старый бэкап: {backup_file}")
|
|
|
|
logger.info("Очистка старых бэкапов завершена")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
|
|
return e
|
|
|
|
|
|
async def create_backup_and_send_to_admins(client) -> None:
|
|
"""
|
|
Создает бэкап и отправляет администраторам через переданный клиент.
|
|
|
|
Args:
|
|
client: Клиент для работы с базой данных
|
|
"""
|
|
await client.login()
|
|
await client.database.export()
|
|
|
|
|
|
async def _send_backup_to_admins(backup_file_path: str) -> None:
|
|
"""
|
|
Отправляет файл бэкапа всем администраторам через Telegram.
|
|
|
|
Args:
|
|
backup_file_path: Путь к файлу бэкапа
|
|
|
|
Raises:
|
|
Exception: При ошибке отправки файла
|
|
"""
|
|
if not backup_file_path or not os.path.exists(backup_file_path):
|
|
raise FileNotFoundError(f"Файл бэкапа не найден: {backup_file_path}")
|
|
|
|
try:
|
|
async with aiofiles.open(backup_file_path, "rb") as backup_file:
|
|
backup_data = await backup_file.read()
|
|
filename = os.path.basename(backup_file_path)
|
|
backup_input_file = BufferedInputFile(file=backup_data, filename=filename)
|
|
|
|
for admin_id in ADMIN_ID:
|
|
try:
|
|
await bot.send_document(chat_id=admin_id, document=backup_input_file)
|
|
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
|
|
except Exception as e:
|
|
logger.error(f"Не удалось отправить бэкап админу {admin_id}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
|
|
raise
|