feat: Добавлена функция восстановления бэкапов
Внесены изменения в API и веб-приложение для поддержки восстановления резервных копий данных. Добавлена новая мутация API для восстановления бэкапов. Добавлена кнопка восстановления в веб-приложении. Добавлена функция прерывания всех запросов при восстановлении бэкапа.
This commit is contained in:
parent
04b9682f1e
commit
8042460557
@ -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,
|
||||
|
||||
@ -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')
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
Loading…
x
Reference in New Issue
Block a user