feat: Добавлена функция восстановления бэкапов

Внесены изменения в API и веб-приложение для поддержки восстановления резервных копий данных. Добавлена новая мутация API для восстановления бэкапов. Добавлена кнопка восстановления в веб-приложении. Добавлена функция прерывания всех запросов при восстановлении бэкапа.
This commit is contained in:
Андрей Дувакин 2025-07-03 08:38:26 +05:00
parent 04b9682f1e
commit 8042460557
26 changed files with 277 additions and 96 deletions

View File

@ -2,7 +2,7 @@ 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.database.session import get_db, engine
from app.domain.entities.responses.backup import BackupResponseEntity
from app.infrastructure.backup_service import BackupService
from app.infrastructure.dependencies import require_admin
@ -79,6 +79,27 @@ async def upload_backup(
return await backup_service.upload_backup(file, user)
@router.post(
'/{backup_id}/restore/',
response_model=BackupResponseEntity,
summary='Restore a backup',
description='Restore a backup by ID, overwriting existing data',
)
async def restore_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.restore_backup(backup_id, engine)
@router.delete(
'/{backup_id}/',
response_model=BackupResponseEntity,

View File

@ -2,14 +2,16 @@ from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class AppointmentFile(BaseModel):
__tablename__ = 'appointment_files'
__table_args__ = {"schema": settings.SCHEMA}
file_path = Column(String, nullable=False)
file_title = Column(String, nullable=False)
appointment_id = Column(Integer, ForeignKey('appointments.id'), nullable=False)
appointment_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointments.id'), nullable=False)
appointment = relationship('Appointment', back_populates='files')

View File

@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class AppointmentType(BaseModel):
__tablename__ = 'appointment_types'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,18 +3,20 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.domain.models.base import BaseModel
from app.settings import settings
class Appointment(BaseModel):
__tablename__ = 'appointments'
__table_args__ = {"schema": settings.SCHEMA}
results = Column(String)
days_until_the_next_appointment = Column(Integer)
appointment_datetime = Column(DateTime, nullable=False, server_default=func.now())
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False)
patient = relationship('Patient', back_populates='appointments')
doctor = relationship('User', back_populates='appointments')

View File

@ -3,14 +3,16 @@ import datetime
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean
from app.domain.models.base import BaseModel
from app.settings import settings
class Backup(BaseModel):
__tablename__ = 'backups'
__table_args__ = {"schema": settings.SCHEMA}
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)
user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)

View File

@ -4,6 +4,7 @@ from sqlalchemy import Column, Integer, ForeignKey, Float, Enum, Boolean
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class SideEnum(PyEnum):
@ -13,6 +14,7 @@ class SideEnum(PyEnum):
class Lens(BaseModel):
__tablename__ = 'lens'
__table_args__ = {"schema": settings.SCHEMA}
tor = Column(Float, nullable=False)
trial = Column(Float, nullable=False)
@ -24,7 +26,7 @@ class Lens(BaseModel):
side = Column(Enum(SideEnum), nullable=False)
issued = Column(Boolean, nullable=False, default=False)
type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False)
type = relationship('LensType', back_populates='lenses')

View File

@ -2,16 +2,18 @@ from sqlalchemy import Column, Integer, ForeignKey, Date
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class LensIssue(BaseModel):
__tablename__ = 'lens_issues'
__table_args__ = {"schema": settings.SCHEMA}
issue_date = Column(Date, nullable=False)
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
lens_id = Column(Integer, ForeignKey('lens.id'), nullable=False)
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
lens_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens.id'), nullable=False)
patient = relationship('Patient', back_populates='lens_issues')
doctor = relationship('User', back_populates='lens_issues')

View File

@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class LensType(BaseModel):
__tablename__ = 'lens_types'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,16 +3,18 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.domain.models.base import BaseModel
from app.settings import settings
class Mailing(BaseModel):
__tablename__ = 'mailing'
__table_args__ = {"schema": settings.SCHEMA}
text = Column(String, nullable=False)
title = Column(String, nullable=False)
datetime = Column(DateTime, nullable=False, default=func.utcnow)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
user = relationship('User', back_populates='mailing')

View File

@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class MailingDeliveryMethod(BaseModel):
__tablename__ = 'mailing_delivery_methods'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(200), nullable=False)

View File

@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class MailingOption(BaseModel):
__tablename__ = 'mailing_options'
__table_args__ = {"schema": settings.SCHEMA}
option_id = Column(Integer, ForeignKey('mailing_delivery_methods.id'), nullable=False)
mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
option_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing_delivery_methods.id'), nullable=False)
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False)
method = relationship('MailingDeliveryMethod', back_populates='mailing')
mailing = relationship('Mailing', back_populates='mailing_options')

View File

@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR, Date, String
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class Patient(BaseModel):
__tablename__ = 'patients'
__table_args__ = {"schema": settings.SCHEMA}
first_name = Column(VARCHAR(200), nullable=False)
last_name = Column(VARCHAR(200), nullable=False)

View File

@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class Recipient(BaseModel):
__tablename__ = 'recipients'
__table_args__ = {"schema": settings.SCHEMA}
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False)
patient = relationship('Patient', back_populates='mailing')
mailing = relationship('Mailing', back_populates='recipients')

View File

@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class Role(BaseModel):
__tablename__ = 'roles'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,17 +3,19 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.domain.models.base import BaseModel
from app.settings import settings
class ScheduledAppointment(BaseModel):
__tablename__ = 'scheduled_appointments'
__table_args__ = {"schema": settings.SCHEMA}
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now())
is_canceled = Column(Boolean, nullable=False, default=False, server_default='false')
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False)
patient = relationship('Patient', back_populates='scheduled_appointments')
doctor = relationship('User', back_populates='scheduled_appointments')

View File

@ -3,10 +3,12 @@ from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.domain.models.lens import SideEnum
from app.settings import settings
class SetContent(BaseModel):
__tablename__ = 'set_contents'
__table_args__ = {"schema": settings.SCHEMA}
tor = Column(Float, nullable=False)
trial = Column(Float, nullable=False)
@ -18,8 +20,8 @@ class SetContent(BaseModel):
side = Column(Enum(SideEnum), nullable=False)
count = Column(Integer, nullable=False)
type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
set_id = Column(Integer, ForeignKey('sets.id'), nullable=False)
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False)
set_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.sets.id'), nullable=False)
type = relationship('LensType', back_populates='contents')
set = relationship('Set', back_populates='contents')

View File

@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
from sqlalchemy.orm import relationship
from app.domain.models.base import BaseModel
from app.settings import settings
class Set(BaseModel):
__tablename__ = 'sets'
__table_args__ = {"schema": settings.SCHEMA}
title = Column(VARCHAR(150), nullable=False, unique=True)

View File

@ -3,10 +3,12 @@ from sqlalchemy.orm import relationship
from werkzeug.security import check_password_hash, generate_password_hash
from app.domain.models.base import BaseModel
from app.settings import settings
class User(BaseModel):
__tablename__ = 'users'
__table_args__ = {"schema": settings.SCHEMA}
first_name = Column(VARCHAR(200), nullable=False)
last_name = Column(VARCHAR(200), nullable=False)
@ -15,7 +17,7 @@ class User(BaseModel):
password = Column(String, nullable=False)
is_blocked = Column(Boolean, nullable=False, default=False, server_default='false')
role_id = Column(Integer, ForeignKey('roles.id'), nullable=False)
role_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.roles.id'), nullable=False)
role = relationship('Role', back_populates='users')

View File

@ -1,14 +1,16 @@
import io
import subprocess
import os
import tarfile
import datetime
from typing import Any, Coroutine, Optional
import io
import os
import shutil
import subprocess
import tarfile
from typing import Optional
from fastapi_maintenance import maintenance_mode_on
import aiofiles
from fastapi import HTTPException, UploadFile
from magic import magic
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
from starlette.responses import FileResponse
from werkzeug.utils import secure_filename
@ -69,6 +71,58 @@ class BackupService:
except subprocess.CalledProcessError as e:
raise HTTPException(500, f"Ошибка создания бэкапа: {e}")
async def restore_backup(self, backup_id: int, engine: AsyncEngine) -> BackupResponseEntity:
try:
async with maintenance_mode_on():
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, 'Файл не найден на диске')
with tarfile.open(backup.path, "r:gz") as tar:
members = tar.getnames()
if "db_dump.sql" not in members or not any(
name.startswith(os.path.basename(self.app_files_dir)) for name in members):
raise HTTPException(400, "Неверная структура архива резервной копии")
if os.path.exists(self.app_files_dir):
shutil.rmtree(self.app_files_dir)
os.makedirs(self.app_files_dir, exist_ok=True)
await engine.dispose()
psql_path = self.pg_dump_path.replace("pg_dump", "psql")
drop_cmd = (
f'"{psql_path}" '
f'-d "{self.db_url}" '
f'-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"'
)
subprocess.run(drop_cmd, shell=True, check=True)
temp_dir = os.path.join(self.backup_dir, f"temp_restore_{backup_id}")
os.makedirs(temp_dir, exist_ok=True)
with tarfile.open(backup.path, "r:gz") as tar:
tar.extractall(temp_dir)
db_dump_path = os.path.join(temp_dir, "db_dump.sql")
restore_cmd = f'"{self.pg_dump_path.replace("pg_dump", "pg_restore")}" -d {self.db_url} --no-owner --no-privileges -Fc "{db_dump_path}"'
subprocess.run(restore_cmd, shell=True, check=True)
extracted_app_files_dir = os.path.join(temp_dir, os.path.basename(self.app_files_dir))
if os.path.exists(extracted_app_files_dir):
shutil.copytree(extracted_app_files_dir, self.app_files_dir, dirs_exist_ok=True)
shutil.rmtree(temp_dir)
return self.model_to_entity(backup)
except subprocess.CalledProcessError as e:
raise HTTPException(500, f"Ошибка восстановления бэкапа: {e}")
except Exception as e:
raise HTTPException(500, f"Ошибка сервера: {str(e)}")
async def get_backup_file_by_id(self, backup_id: int) -> FileResponse:
backup = await self.backup_repository.get_by_id(backup_id)

View File

@ -1,4 +1,5 @@
from fastapi import FastAPI
from fastapi_maintenance import MaintenanceModeMiddleware
from starlette.middleware.cors import CORSMiddleware
from app.controllers.appointment_files_router import router as appointment_files_router
@ -29,6 +30,7 @@ def start_app():
allow_methods=['*'],
allow_headers=['*'],
)
# api_app.add_middleware(MaintenanceModeMiddleware, enable_maintenance=False)
api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files',
tags=['appointment_files'])

View File

@ -12,6 +12,7 @@ class Settings(BaseSettings):
BACKUP_DIR: str = 'backups'
BACKUP_DB_URL: str
PG_DUMP_PATH: str
SCHEMA: str = 'public'
class Config:
env_file = '.env'

View File

@ -11,4 +11,5 @@ Werkzeug==3.1.3
pyjwt==2.10.1
python-magic==0.4.27
aiofiles==24.1.0
python-multipart==0.0.20
python-multipart==0.0.20
fastapi-maintenance==0.0.4

View File

@ -40,7 +40,12 @@ export const backupsApi = createApi({
},
invalidatesTags: ['Backup'],
}),
restoreBackup: builder.mutation({
query: (backupId) => ({
url: `/backups/${backupId}/restore/`,
method: 'POST',
}),
}),
}),
});
@ -49,4 +54,5 @@ export const {
useCreateBackupMutation,
useDeleteBackupMutation,
useUploadBackupMutation,
useRestoreBackupMutation,
} = backupsApi;

View File

@ -1,29 +1,29 @@
import { useDispatch, useSelector } from "react-redux";
import { setSelectedAppointment } from "../../../Redux/Slices/appointmentsSlice.js";
import {useDispatch, useSelector} from "react-redux";
import {setSelectedAppointment} from "../../../Redux/Slices/appointmentsSlice.js";
import dayjs from "dayjs";
import { useState } from "react";
import { useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation } from "../../../Api/appointmentFilesApi.js";
import { baseQueryWithAuth } from "../../../Api/baseQuery.js";
import { notification } from "antd";
import {useState} from "react";
import {useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js";
import {notification} from "antd";
import CONFIG from "../../../Core/сonfig.js";
const useAppointmentView = () => {
const dispatch = useDispatch();
const { selectedAppointment } = useSelector((state) => state.appointmentsUI);
const {selectedAppointment} = useSelector((state) => state.appointmentsUI);
const { data: files = [], isLoading: isFilesLoading } = useGetAppointmentFilesQuery(
const {data: files = [], isLoading: isFilesLoading} = useGetAppointmentFilesQuery(
selectedAppointment?.id,
{ skip: !selectedAppointment?.id }
{skip: !selectedAppointment?.id}
);
const [deleteAppointmentFile, { isLoading: isDeletingFile }] = useDeleteAppointmentFileMutation();
const [deleteAppointmentFile, {isLoading: isDeletingFile}] = useDeleteAppointmentFileMutation();
const [downloadingFiles, setDownloadingFiles] = useState({});
const [deletingFiles, setDeletingFiles] = useState({});
const modalWidth = 700;
const blockStyle = { marginBottom: 16 };
const footerRowStyle = { marginTop: 16 };
const footerButtonStyle = { marginRight: 8 };
const blockStyle = {marginBottom: 16};
const footerRowStyle = {marginTop: 16};
const footerButtonStyle = {marginRight: 8};
const labels = {
title: "Просмотр приема",
@ -81,54 +81,84 @@ const useAppointmentView = () => {
const downloadFile = async (fileId, fileName) => {
try {
setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
const { url, ...options } = await baseQueryWithAuth(
{
url: `/appointment_files/${fileId}/file/`,
method: 'GET',
credentials: 'include',
},
{},
{}
);
const response = await fetch(url, {
...options,
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
const token = localStorage.getItem('access_token');
if (!token) {
notification.error({
message: "Ошибка при скачивании файла",
description: "Не удалось загрузить файл.",
message: "Ошибка",
description: "Токен не найден",
placement: "topRight",
});
return;
}
const response = await fetch(`${CONFIG.BASE_URL}appointment_files/${fileId}/file/`, {
method: 'GET',
credentials: 'include',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
const errorText = await response.text();
notification.error({
message: "Ошибка",
description: errorText || "Не удалось скачать файл",
placement: "topRight",
});
return;
}
const contentType = response.headers.get('content-type');
if (!contentType || contentType.includes('text/html')) {
const errorText = await response.text();
notification.error({
message: "Ошибка",
description: errorText || "Не удалось скачать файл",
placement: "topRight",
});
return;
}
let safeFileName = fileName || "file";
if (!safeFileName.match(/\.[a-zA-Z0-9]+$/)) {
if (contentType.includes('application/pdf')) {
safeFileName += '.pdf';
} else if (contentType.includes('image/jpeg')) {
safeFileName += '.jpg';
} else if (contentType.includes('image/png')) {
safeFileName += '.png';
} else {
safeFileName += '.bin';
}
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.setAttribute("download", fileName || "file");
link.setAttribute("download", safeFileName);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error("Error downloading file:", error);
console.error("Error downloading file:", error); // Отладка
notification.error({
message: "Ошибка при скачивании файлов",
description: "Не удалось загрузить файл.",
message: "Ошибка",
description: error.message || "Не удалось скачать файл",
placement: "topRight",
});
} finally {
setDownloadingFiles((prev) => ({ ...prev, [fileId]: false }));
setDownloadingFiles((prev) => ({...prev, [fileId]: false}));
}
};
const deleteFile = async (fileId, fileName) => {
try {
setDeletingFiles((prev) => ({ ...prev, [fileId]: true }));
setDeletingFiles((prev) => ({...prev, [fileId]: true}));
await deleteAppointmentFile(fileId).unwrap();
notification.success({
message: "Файл удален",
@ -143,7 +173,7 @@ const useAppointmentView = () => {
placement: "topRight",
});
} finally {
setDeletingFiles((prev) => ({ ...prev, [fileId]: false }));
setDeletingFiles((prev) => ({...prev, [fileId]: false}));
}
};

View File

@ -1,5 +1,5 @@
import {Button, Divider, List, Result, Space, Tooltip, Typography, Upload} from "antd";
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} from "@ant-design/icons";
import {Button, Divider, List, Popconfirm, Result, Space, Tooltip, Typography, Upload} from "antd";
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined} from "@ant-design/icons";
import useBackupManageTab from "./useBackupManageTab.js";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
@ -11,7 +11,7 @@ const BackupManageTab = () => {
}
if (backupManageTabData.isErrorBackups) {
return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>
return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>;
}
return (
@ -22,7 +22,6 @@ const BackupManageTab = () => {
icon={<CloudDownloadOutlined/>}
onClick={backupManageTabData.createBackupHandler}
loading={backupManageTabData.isCreatingBackup}
>
Создать резервную копию
</Button>
@ -46,33 +45,47 @@ const BackupManageTab = () => {
renderItem={(backup) => (
<List.Item>
<Typography.Text>{backup.filename}</Typography.Text>
<Divider type={"vertical"}/>
<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>
<Space>
<Tooltip title="Скачать резервную копию">
<Button
type="primary"
icon={<CloudDownloadOutlined/>}
onClick={() => backupManageTabData.downloadBackupHandler(backup.id, backup.filename)}
loading={backupManageTabData.isDownloadingBackup}
>
Скачать
</Button>
</Tooltip>
<Tooltip title="Восстановить резервную копию">
<Popconfirm title={"Вы действительно хотите восстановить резервную копию? Существующие данные будут утеряны навсегда."}
onConfirm={() => backupManageTabData.restoreBackupHandler(backup.id)}>
<Button
type="primary"
icon={<ReloadOutlined/>}
loading={backupManageTabData.isRestoringBackup}
>
Восстановить
</Button>
</Popconfirm>
</Tooltip>
<Tooltip title="Удалить резервную копию">
<Button
type="primary"
icon={<DeleteOutlined/>}
onClick={() => backupManageTabData.deleteBackupHandler(backup.id)}
loading={backupManageTabData.isDeletingBackup}
danger
>
Удалить
</Button>
</Tooltip>
</Space>
</List.Item>
)}
/>
</>
);
};

View File

@ -1,7 +1,9 @@
import {
useCreateBackupMutation,
useDeleteBackupMutation,
useGetBackupsQuery, useUploadBackupMutation
useGetBackupsQuery,
useUploadBackupMutation,
useRestoreBackupMutation,
} from "../../../../../Api/backupsApi.js";
import {useDispatch} from "react-redux";
import {notification} from "antd";
@ -10,13 +12,13 @@ import CONFIG from "../../../../../Core/сonfig.js";
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 [restoreBackup, {isLoading: isRestoringBackup}] = useRestoreBackupMutation();
const [isDownloadingBackup, setDownloadingFiles] = useState(false);
@ -107,6 +109,20 @@ const useBackupManageTab = () => {
}
};
const restoreBackupHandler = async (backupId) => {
try {
await restoreBackup(backupId).unwrap();
notification.success({
message: "Успех", description: "Резервная копия успешно восстановлена", placement: "topRight",
});
} catch (e) {
notification.error({
message: "Ошибка",
description: e?.data?.detail || "Не удалось восстановить резервную копию",
placement: "topRight",
});
}
};
const deleteBackupHandler = async (backupId) => {
try {
@ -142,6 +158,7 @@ const useBackupManageTab = () => {
placement: "topRight",
});
} catch (e) {
console.error("Upload error:", e); // Отладка
notification.error({
message: "Ошибка",
description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию",
@ -158,11 +175,13 @@ const useBackupManageTab = () => {
isDownloadingBackup,
isDeletingBackup,
isUploadingBackup,
isRestoringBackup,
createBackupHandler,
downloadBackupHandler,
restoreBackupHandler,
deleteBackupHandler,
uploadBackupHandler,
}
};
};
export default useBackupManageTab;