Compare commits
5 Commits
2447bc53af
...
b52c0d16fa
| Author | SHA1 | Date | |
|---|---|---|---|
| b52c0d16fa | |||
| c70b109851 | |||
| 039d7cb0cb | |||
| 22c1a9ca80 | |||
| 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 fastapi import APIRouter, Depends, File, UploadFile
|
||||
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,90 @@ 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.post(
|
||||
'/upload/',
|
||||
response_model=BackupResponseEntity,
|
||||
summary='Upload a backup',
|
||||
description='Upload a backup',
|
||||
)
|
||||
async def upload_backup(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
backup_service = BackupService(
|
||||
db,
|
||||
backup_dir=settings.BACKUP_DIR,
|
||||
app_files_dir=settings.FILE_UPLOAD_DIR,
|
||||
)
|
||||
return await backup_service.upload_backup(file, user)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
"""0007 добавил в backups поле is_by_user
|
||||
|
||||
Revision ID: 4f3877d7a2b1
|
||||
Revises: b58238896c0f
|
||||
Create Date: 2025-07-01 18:54:27.796866
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4f3877d7a2b1'
|
||||
down_revision: Union[str, None] = 'b58238896c0f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('backups', sa.Column('is_by_user', sa.Boolean(), nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('backups', 'is_by_user')
|
||||
# ### end Alembic commands ###
|
||||
13
api/app/domain/entities/responses/backup.py
Normal file
13
api/app/domain/entities/responses/backup.py
Normal file
@ -0,0 +1,13 @@
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BackupResponseEntity(BaseModel):
|
||||
id: int
|
||||
timestamp: datetime.datetime
|
||||
path: str
|
||||
filename: str
|
||||
is_by_user: bool
|
||||
|
||||
user_id: int
|
||||
@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey
|
||||
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean
|
||||
|
||||
from app.domain.models.base import BaseModel
|
||||
|
||||
@ -11,7 +11,6 @@ class Backup(BaseModel):
|
||||
timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||
path = Column(String, nullable=False)
|
||||
filename = Column(String, nullable=False)
|
||||
is_by_user = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
|
||||
|
||||
|
||||
@ -1,40 +1,174 @@
|
||||
import io
|
||||
import subprocess
|
||||
import os
|
||||
import tarfile
|
||||
import datetime
|
||||
from fastapi import HTTPException
|
||||
from typing import Any, Coroutine, Optional
|
||||
|
||||
import aiofiles
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from magic import magic
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import FileResponse
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.application.backups_repository import BackupsRepository
|
||||
from app.domain.entities.responses.backup import BackupResponseEntity
|
||||
from app.domain.models import Backup, User
|
||||
|
||||
|
||||
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 upload_backup(self, file: UploadFile, user: User) -> BackupResponseEntity:
|
||||
file_bytes = await file.read()
|
||||
file.file.seek(0)
|
||||
self.validate_file_type(file)
|
||||
|
||||
if not self.validate_backup_archive(file_bytes, self.app_files_dir):
|
||||
raise HTTPException(400, "Неверная структура архива резервной копии")
|
||||
|
||||
filename = self.generate_filename(file)
|
||||
backup_path = os.path.join(self.backup_dir, filename)
|
||||
async with aiofiles.open(backup_path, 'wb') as out_file:
|
||||
await out_file.write(file_bytes)
|
||||
|
||||
backup_record = Backup(
|
||||
filename=filename,
|
||||
path=backup_path,
|
||||
user_id=user.id,
|
||||
is_by_user=True,
|
||||
)
|
||||
await self.backup_repository.create(backup_record)
|
||||
return self.model_to_entity(backup_record)
|
||||
|
||||
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 generate_filename(file: UploadFile) -> str:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return secure_filename(f"uploaded_{timestamp}_{file.filename}")
|
||||
|
||||
@staticmethod
|
||||
def validate_backup_archive(file_bytes: bytes, expected_app_files_dir_name: str) -> bool:
|
||||
try:
|
||||
with tarfile.open(fileobj=io.BytesIO(file_bytes), mode="r:gz") as tar:
|
||||
members = tar.getnames()
|
||||
|
||||
if "db_dump.sql" not in members:
|
||||
return False
|
||||
|
||||
if not any(name.startswith(expected_app_files_dir_name) for name in members):
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def model_to_entity(backup: Backup):
|
||||
return BackupResponseEntity(
|
||||
id=backup.id,
|
||||
timestamp=backup.timestamp,
|
||||
path=backup.path,
|
||||
filename=backup.filename,
|
||||
is_by_user=backup.is_by_user,
|
||||
user_id=backup.user_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_file_type(file: UploadFile):
|
||||
mime = magic.Magic(mime=True)
|
||||
file_type = mime.from_buffer(file.file.read(1024))
|
||||
file.file.seek(0)
|
||||
if file_type not in ["application/zip", "application/gzip", "application/x-gzip"]:
|
||||
raise HTTPException(400, "Неправильный формат файла")
|
||||
|
||||
@staticmethod
|
||||
def get_media_type(filename: str) -> str:
|
||||
extension = filename.split('.')[-1].lower()
|
||||
if extension in ['zip']:
|
||||
return "application/zip"
|
||||
|
||||
if extension == 'tar.gz':
|
||||
return "application/gzip"
|
||||
|
||||
return "application/octet-stream"
|
||||
|
||||
52
web-app/src/Api/backupsApi.js
Normal file
52
web-app/src/Api/backupsApi.js
Normal file
@ -0,0 +1,52 @@
|
||||
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||
|
||||
|
||||
export const backupsApi = createApi({
|
||||
reducerPath: 'backupsApi',
|
||||
baseQuery: baseQueryWithAuth,
|
||||
tagTypes: ['Backup'],
|
||||
endpoints: (builder) => ({
|
||||
getBackups: builder.query({
|
||||
query: () => `/backups/`,
|
||||
providesTags: ['Backup'],
|
||||
}),
|
||||
createBackup: builder.mutation({
|
||||
query: () => ({
|
||||
url: '/backups/',
|
||||
method: 'POST',
|
||||
}),
|
||||
invalidatesTags: ['Backup'],
|
||||
}),
|
||||
deleteBackup: builder.mutation({
|
||||
query: (backupId) => ({
|
||||
url: `/backups/${backupId}/`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: ['Backup'],
|
||||
}),
|
||||
uploadBackup: builder.mutation({
|
||||
query: (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return {
|
||||
url: '/backups/upload/',
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
formData: true,
|
||||
body: formData,
|
||||
};
|
||||
},
|
||||
invalidatesTags: ['Backup'],
|
||||
}),
|
||||
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetBackupsQuery,
|
||||
useCreateBackupMutation,
|
||||
useDeleteBackupMutation,
|
||||
useUploadBackupMutation,
|
||||
} = backupsApi;
|
||||
@ -9,8 +9,7 @@ export const baseQuery = fetchBaseQuery({
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (endpoint === 'uploadAppointmentFile') {
|
||||
if (endpoint === 'uploadAppointmentFile' || endpoint === 'uploadBackup') {
|
||||
const mutation = getState()?.api?.mutations?.[Object.keys(getState()?.api?.mutations || {})[0]];
|
||||
if (mutation?.body instanceof FormData) {
|
||||
headers.delete('Content-Type');
|
||||
|
||||
@ -202,7 +202,7 @@ const AppointmentFormModal = () => {
|
||||
fileList={appointmentFormModalUI.draftFiles}
|
||||
beforeUpload={(file) => {
|
||||
appointmentFormModalUI.handleAddFile(file);
|
||||
return false; // Prevent auto-upload
|
||||
return false;
|
||||
}}
|
||||
onRemove={(file) => appointmentFormModalUI.handleRemoveFile(file)}
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
||||
|
||||
@ -1,37 +1,78 @@
|
||||
import {Button, Space, Spin, Typography, Upload} from "antd";
|
||||
import {CloudDownloadOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import {Button, Divider, List, Result, Space, Tooltip, Typography, Upload} from "antd";
|
||||
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import useBackupManageTab from "./useBackupManageTab.js";
|
||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const BackupManageTab = () => {
|
||||
const backupManageTabData = useBackupManageTab();
|
||||
|
||||
if (backupManageTabData.isLoadingBackups) {
|
||||
return <LoadingIndicator/>;
|
||||
}
|
||||
|
||||
if (backupManageTabData.isErrorBackups) {
|
||||
return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={false}>
|
||||
<Typography>
|
||||
<Typography.Title level={4}>Управление резервными копиями</Typography.Title>
|
||||
<Typography.Paragraph>
|
||||
Здесь вы можете создать резервную копию системы и восстановить её из архива.
|
||||
</Typography.Paragraph>
|
||||
</Typography>
|
||||
<Space direction="vertical" size="large" style={{width: "100%"}}>
|
||||
<>
|
||||
<Space direction="horizontal" size="large" style={{width: "100%"}}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudDownloadOutlined/>}
|
||||
// onClick={handleCreateBackup}
|
||||
// disabled={loading}
|
||||
block
|
||||
onClick={backupManageTabData.createBackupHandler}
|
||||
loading={backupManageTabData.isCreatingBackup}
|
||||
|
||||
>
|
||||
Создать и скачать бэкап
|
||||
Создать резервную копию
|
||||
</Button>
|
||||
<Upload
|
||||
// beforeUpload={handleUpload}
|
||||
showUploadList={false}
|
||||
accept=".tar.gz,.zip"
|
||||
// disabled={loading}
|
||||
disabled={backupManageTabData.isUploadingBackup}
|
||||
beforeUpload={(file) => {
|
||||
backupManageTabData.uploadBackupHandler(file);
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined/>} block>
|
||||
Загрузить бэкап для восстановления
|
||||
Загрузить резервную копию для восстановления
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
</Spin>
|
||||
<Divider/>
|
||||
<List
|
||||
dataSource={backupManageTabData.backups}
|
||||
renderItem={(backup) => (
|
||||
<List.Item>
|
||||
<Typography.Text>{backup.filename}</Typography.Text>
|
||||
<Typography.Text>{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}</Typography.Text>
|
||||
<Tooltip title="Скачать резервную копию">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloudDownloadOutlined/>}
|
||||
onClick={() => backupManageTabData.downloadBackupHandler(backup.id, backup.filename)}
|
||||
loading={backupManageTabData.isDownloadingBackup}
|
||||
>
|
||||
Скачать
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Удалить резервную копию">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DeleteOutlined/>}
|
||||
onClick={() => backupManageTabData.deleteBackupHandler(backup.id)}
|
||||
loading={backupManageTabData.isDeletingBackup}
|
||||
danger
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,144 @@
|
||||
import {
|
||||
useCreateBackupMutation,
|
||||
useDeleteBackupMutation,
|
||||
useGetBackupsQuery, useUploadBackupMutation
|
||||
} from "../../../../../Api/backupsApi.js";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {notification} from "antd";
|
||||
import {baseQueryWithAuth} from "../../../../../Api/baseQuery.js";
|
||||
import {useState} from "react";
|
||||
|
||||
|
||||
const useBackupManageTab = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, {
|
||||
pollingInterval: 60000,
|
||||
});
|
||||
const [createBackup, {isLoading: isCreatingBackup}] = useCreateBackupMutation();
|
||||
const [deleteBackup, {isLoading: isDeletingBackup}] = useDeleteBackupMutation();
|
||||
const [uploadBackup, {isLoading: isUploadingBackup}] = useUploadBackupMutation();
|
||||
|
||||
const [isDownloadingBackup, setDownloadingFiles] = useState(false);
|
||||
|
||||
const createBackupHandler = async () => {
|
||||
try {
|
||||
await createBackup().unwrap();
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Резервная копия успешно создана",
|
||||
placement: "topRight",
|
||||
});
|
||||
} catch (e) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: e?.data?.detail || "Не удалось создать резервную копию",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const downloadBackupHandler = async (backupId, fileName) => {
|
||||
try {
|
||||
setDownloadingFiles(true);
|
||||
const {url, ...options} = await baseQueryWithAuth(
|
||||
{
|
||||
url: `/backups/${backupId}/`,
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
notification.error({
|
||||
message: "Ошибка при скачивании файла",
|
||||
description: "Не удалось загрузить файл.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.setAttribute("download", fileName || "file");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
setDownloadingFiles(false);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: e?.data?.detail || "Не удалось загрузить резервную копию",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBackupHandler = async (backupId) => {
|
||||
try {
|
||||
await deleteBackup(backupId).unwrap();
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Резервная копия успешно удалена",
|
||||
placement: "topRight",
|
||||
});
|
||||
} catch (e) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: e?.data?.detail || "Не удалось удалить резервную копию",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const uploadBackupHandler = async (file) => {
|
||||
try {
|
||||
if (!file || !(file instanceof File)) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: "Файл не выбран",
|
||||
placement: "topRight",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await uploadBackup(file).unwrap();
|
||||
notification.success({
|
||||
message: "Успех",
|
||||
description: "Резервная копия успешно загружена",
|
||||
placement: "topRight",
|
||||
});
|
||||
} catch (e) {
|
||||
notification.error({
|
||||
message: "Ошибка",
|
||||
description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
backups,
|
||||
isLoadingBackups,
|
||||
isErrorBackups,
|
||||
isCreatingBackup,
|
||||
isDownloadingBackup,
|
||||
isDeletingBackup,
|
||||
isUploadingBackup,
|
||||
createBackupHandler,
|
||||
downloadBackupHandler,
|
||||
deleteBackupHandler,
|
||||
uploadBackupHandler,
|
||||
}
|
||||
};
|
||||
|
||||
export default useBackupManageTab;
|
||||
@ -21,6 +21,7 @@ import {rolesApi} from "../Api/rolesApi.js";
|
||||
import adminReducer from "./Slices/adminSlice.js";
|
||||
import {registerApi} from "../Api/registerApi.js";
|
||||
import {appointmentFilesApi} from "../Api/appointmentFilesApi.js";
|
||||
import {backupsApi} from "../Api/backupsApi.js";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@ -60,6 +61,8 @@ export const store = configureStore({
|
||||
[registerApi.reducerPath]: registerApi.reducer,
|
||||
|
||||
[appointmentFilesApi.reducerPath]: appointmentFilesApi.reducer,
|
||||
|
||||
[backupsApi.reducerPath]: backupsApi.reducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => (
|
||||
getDefaultMiddleware().concat(
|
||||
@ -77,6 +80,7 @@ export const store = configureStore({
|
||||
rolesApi.middleware,
|
||||
registerApi.middleware,
|
||||
appointmentFilesApi.middleware,
|
||||
backupsApi.middleware,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user