feat: Добавлена функциональность резервного копирования
This commit is contained in:
parent
2447bc53af
commit
1a12d389fc
32
api/app/application/backups_repository.py
Normal file
32
api/app/application/backups_repository.py
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import Sequence, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import Backup
|
||||
|
||||
|
||||
class BackupsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_all(self) -> Sequence[Backup]:
|
||||
stmt = select(Backup)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_id(self, backup_id: int) -> Optional[Backup]:
|
||||
stmt = select(Backup).filter_by(id=backup_id)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().first()
|
||||
|
||||
async def create(self, backup: Backup) -> Backup:
|
||||
self.db.add(backup)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(backup)
|
||||
return backup
|
||||
|
||||
async def delete(self, backup: Backup) -> Backup:
|
||||
await self.db.delete(backup)
|
||||
await self.db.commit()
|
||||
return backup
|
||||
@ -1,6 +1,9 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from app.database.session import get_db
|
||||
from app.domain.entities.responses.backup import BackupResponseEntity
|
||||
from app.infrastructure.backup_service import BackupService
|
||||
from app.infrastructure.dependencies import require_admin
|
||||
from app.settings import settings
|
||||
@ -8,19 +11,71 @@ from app.settings import settings
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/',
|
||||
response_model=list[BackupResponseEntity],
|
||||
summary='Get all backups',
|
||||
description='Get all backups',
|
||||
)
|
||||
async def get_all_backups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
backup_service = BackupService(db)
|
||||
return await backup_service.get_all_backups()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{backup_id}/',
|
||||
response_model=BackupResponseEntity,
|
||||
summary='Get a backup file',
|
||||
description='Get a backup file',
|
||||
)
|
||||
async def get_backup(
|
||||
backup_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
backup_service = BackupService(db)
|
||||
return await backup_service.get_backup_file_by_id(backup_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/create/",
|
||||
response_class=FileResponse,
|
||||
"/",
|
||||
response_model=BackupResponseEntity,
|
||||
summary="Create backup",
|
||||
description="Create backup",
|
||||
)
|
||||
async def create_backup(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
backup_service = BackupService(
|
||||
db_url=settings.BACKUP_DB_URL,
|
||||
app_files_dir=settings.FILE_UPLOAD_DIR,
|
||||
backup_dir=settings.BACKUP_DIR,
|
||||
pg_dump_path=settings.PG_DUMP_PATH,
|
||||
db,
|
||||
settings.BACKUP_DB_URL,
|
||||
settings.FILE_UPLOAD_DIR,
|
||||
settings.BACKUP_DIR,
|
||||
settings.PG_DUMP_PATH,
|
||||
)
|
||||
return await backup_service.create_backup()
|
||||
return await backup_service.create_backup(user.id)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{backup_id}/',
|
||||
response_model=BackupResponseEntity,
|
||||
summary='Delete backup',
|
||||
description='Delete backup',
|
||||
)
|
||||
async def delete_backup(
|
||||
backup_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
backup_service = BackupService(
|
||||
db,
|
||||
settings.BACKUP_DB_URL,
|
||||
settings.FILE_UPLOAD_DIR,
|
||||
settings.BACKUP_DIR,
|
||||
settings.PG_DUMP_PATH,
|
||||
)
|
||||
return await backup_service.delete_backup(backup_id)
|
||||
|
||||
12
api/app/domain/entities/responses/backup.py
Normal file
12
api/app/domain/entities/responses/backup.py
Normal file
@ -0,0 +1,12 @@
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BackupResponseEntity(BaseModel):
|
||||
id: int
|
||||
timestamp: datetime.datetime
|
||||
path: str
|
||||
filename: str
|
||||
|
||||
user_id: int
|
||||
@ -2,39 +2,106 @@ 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_url: str, app_files_dir: str, backup_dir: str, pg_dump_path: str):
|
||||
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
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
async def create_backup(self) -> FileResponse:
|
||||
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}"
|
||||
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(f"{backup_path}.tar.gz", "w:gz") as tar:
|
||||
tar.add(self.app_files_dir, arcname=self.app_files_dir)
|
||||
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")
|
||||
|
||||
os.remove(db_dump_path)
|
||||
return FileResponse(
|
||||
f"{backup_path}.tar.gz",
|
||||
media_type="application/gzip",
|
||||
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:
|
||||
print(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,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user