Рефакторинг и улучшение модуля резервного копирования базы данных
- Добавлена расширенная типизация и аннотации типов - Улучшена обработка ошибок и логирование в функциях бэкапа - Оптимизирован процесс создания и очистки резервных копий с использованием pathlib - Добавлены информативные docstring комментарии - Усовершенствована отправка бэкапов администраторам с обработкой исключений - Использованы современные подходы к работе с файловой системой и переменными окружения
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from aiogram.types import BufferedInputFile
|
||||
import aiofiles
|
||||
|
||||
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
|
||||
from logger import logger
|
||||
from bot import bot
|
||||
|
||||
|
||||
async def backup_database() -> Exception | None:
|
||||
async def backup_database() -> Optional[Exception]:
|
||||
"""
|
||||
Создает резервную копию базы данных и отправляет ее администраторам.
|
||||
|
||||
Returns:
|
||||
Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении
|
||||
"""
|
||||
backup_file_path, exception = _create_database_backup()
|
||||
|
||||
if exception:
|
||||
@@ -29,83 +39,121 @@ async def backup_database() -> Exception | None:
|
||||
return e
|
||||
|
||||
|
||||
def _create_database_backup() -> tuple[str | None, Exception | None]:
|
||||
def _create_database_backup() -> Tuple[Optional[str], Optional[Exception]]:
|
||||
"""
|
||||
Создает резервную копию базы данных PostgreSQL.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[str], Optional[Exception]]: Путь к файлу бэкапа и исключение (если произошла ошибка)
|
||||
"""
|
||||
date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S")
|
||||
|
||||
if not os.path.exists(BACK_DIR):
|
||||
os.makedirs(BACK_DIR)
|
||||
# Создаем директорию для бэкапов, если она не существует
|
||||
backup_dir = Path(BACK_DIR)
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filename = os.path.join(BACK_DIR, f"{DB_NAME}-backup-{date_formatted}.sql")
|
||||
filename = backup_dir / f"{DB_NAME}-backup-{date_formatted}.sql"
|
||||
|
||||
try:
|
||||
# Устанавливаем пароль PostgreSQL через переменную окружения
|
||||
os.environ["PGPASSWORD"] = DB_PASSWORD
|
||||
|
||||
# Запускаем pg_dump для создания бэкапа
|
||||
subprocess.run(
|
||||
[
|
||||
"pg_dump",
|
||||
"-U",
|
||||
DB_USER,
|
||||
"-h",
|
||||
PG_HOST,
|
||||
"-p",
|
||||
PG_PORT,
|
||||
"-F",
|
||||
"c",
|
||||
"-f",
|
||||
filename,
|
||||
"-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 filename, None
|
||||
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:
|
||||
del os.environ["PGPASSWORD"]
|
||||
# Удаляем переменную окружения с паролем
|
||||
if "PGPASSWORD" in os.environ:
|
||||
del os.environ["PGPASSWORD"]
|
||||
|
||||
|
||||
def _cleanup_old_backups() -> None | Exception:
|
||||
def _cleanup_old_backups() -> Optional[Exception]:
|
||||
"""
|
||||
Удаляет бэкапы старше 3 дней.
|
||||
|
||||
Returns:
|
||||
Optional[Exception]: Исключение в случае ошибки или None при успешном выполнении
|
||||
"""
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"find",
|
||||
BACK_DIR,
|
||||
"-type",
|
||||
"f",
|
||||
"-name",
|
||||
"*.sql",
|
||||
"-mtime",
|
||||
"+3",
|
||||
"-exec",
|
||||
"rm",
|
||||
"{}",
|
||||
";",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
logger.info("Старые бэкапы удалены.")
|
||||
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 subprocess.CalledProcessError as e:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
|
||||
return e
|
||||
|
||||
|
||||
async def create_backup_and_send_to_admins(xui) -> None:
|
||||
await xui.login()
|
||||
await xui.database.export()
|
||||
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:
|
||||
import aiofiles
|
||||
|
||||
from bot import bot
|
||||
|
||||
async with aiofiles.open(backup_file_path, "rb") as backup_file:
|
||||
backup_data = await backup_file.read()
|
||||
backup_input_file = BufferedInputFile(file=backup_data, filename=os.path.basename(backup_file_path))
|
||||
filename = os.path.basename(backup_file_path)
|
||||
backup_input_file = BufferedInputFile(file=backup_data, filename=filename)
|
||||
|
||||
# Отправляем файл каждому администратору
|
||||
for admin_id in ADMIN_ID:
|
||||
await bot.send_document(chat_id=admin_id, document=backup_input_file)
|
||||
logger.info(f"Бэкап базы данных отправлен админу: {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
|
||||
|
||||
Reference in New Issue
Block a user