import subprocess import os import tarfile import datetime from typing import Any, Coroutine, Optional from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from starlette.responses import FileResponse from app.application.backups_repository import BackupsRepository from app.domain.entities.responses.backup import BackupResponseEntity from app.domain.models import Backup class BackupService: def __init__( self, db: AsyncSession, db_url: str = None, app_files_dir: str = None, backup_dir: str = None, pg_dump_path: str = None ): self.backup_repository = BackupsRepository(db) self.db_url = db_url self.app_files_dir = app_files_dir self.backup_dir = backup_dir self.pg_dump_path = pg_dump_path if backup_dir: os.makedirs(backup_dir, exist_ok=True) async def get_all_backups(self) -> list[BackupResponseEntity]: backups = await self.backup_repository.get_all() return [ self.model_to_entity(backup) for backup in backups ] async def create_backup(self, user_id: int) -> BackupResponseEntity: try: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_name = f"backup_{timestamp}.tar.gz" backup_path = os.path.join(self.backup_dir, backup_name) db_dump_path = f"{os.getcwd()}/{backup_path}.sql" dump_cmd = f'"{self.pg_dump_path}" -Fc -d {self.db_url} -f "{db_dump_path}"' subprocess.run(dump_cmd, shell=True, check=True) with tarfile.open(backup_path, "w:gz") as tar: tar.add(self.app_files_dir, arcname=os.path.basename(self.app_files_dir)) tar.add(db_dump_path, arcname="db_dump.sql") backup_record = Backup( filename=backup_name, path=backup_path, user_id=user_id, ) await self.backup_repository.create(backup_record) os.remove(db_dump_path) return self.model_to_entity(backup_record) except subprocess.CalledProcessError as e: raise HTTPException(500, f"Ошибка создания бэкапа: {e}") async def get_backup_file_by_id(self, backup_id: int) -> FileResponse: backup = await self.backup_repository.get_by_id(backup_id) if not backup: raise HTTPException(404, 'Резервная копия с таким id не найдена') if not os.path.exists(backup.path): raise HTTPException(404, 'Файл не найден на диске') return FileResponse( backup.path, media_type="application/gzip", filename=backup.filename, ) async def delete_backup(self, backup_id: int) -> Optional[BackupResponseEntity]: backup = await self.backup_repository.get_by_id(backup_id) if not backup: raise HTTPException(404, 'Резервная копия с таким id не найдена') if not os.path.exists(backup.path): raise HTTPException(404, 'Файл не найден на диске') if os.path.exists(backup.path): os.remove(backup.path) return self.model_to_entity( await self.backup_repository.delete(backup) ) @staticmethod def model_to_entity(backup: Backup): return BackupResponseEntity( id=backup.id, timestamp=backup.timestamp, path=backup.path, filename=backup.filename, user_id=backup.user_id, )