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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from starlette.responses import FileResponse
|
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.domain.entities.responses.backup import BackupResponseEntity
|
||||||
from app.infrastructure.backup_service import BackupService
|
from app.infrastructure.backup_service import BackupService
|
||||||
from app.infrastructure.dependencies import require_admin
|
from app.infrastructure.dependencies import require_admin
|
||||||
@ -79,6 +79,27 @@ async def upload_backup(
|
|||||||
return await backup_service.upload_backup(file, user)
|
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(
|
@router.delete(
|
||||||
'/{backup_id}/',
|
'/{backup_id}/',
|
||||||
response_model=BackupResponseEntity,
|
response_model=BackupResponseEntity,
|
||||||
|
|||||||
@ -2,14 +2,16 @@ from sqlalchemy import Column, Integer, String, ForeignKey
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class AppointmentFile(BaseModel):
|
class AppointmentFile(BaseModel):
|
||||||
__tablename__ = 'appointment_files'
|
__tablename__ = 'appointment_files'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
file_path = Column(String, nullable=False)
|
file_path = Column(String, nullable=False)
|
||||||
file_title = 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')
|
appointment = relationship('Appointment', back_populates='files')
|
||||||
@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class AppointmentType(BaseModel):
|
class AppointmentType(BaseModel):
|
||||||
__tablename__ = 'appointment_types'
|
__tablename__ = 'appointment_types'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
title = Column(VARCHAR(150), nullable=False, unique=True)
|
title = Column(VARCHAR(150), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|||||||
@ -3,18 +3,20 @@ from sqlalchemy.orm import relationship
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Appointment(BaseModel):
|
class Appointment(BaseModel):
|
||||||
__tablename__ = 'appointments'
|
__tablename__ = 'appointments'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
results = Column(String)
|
results = Column(String)
|
||||||
days_until_the_next_appointment = Column(Integer)
|
days_until_the_next_appointment = Column(Integer)
|
||||||
appointment_datetime = Column(DateTime, nullable=False, server_default=func.now())
|
appointment_datetime = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
|
|
||||||
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
|
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
|
||||||
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
|
||||||
type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
|
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False)
|
||||||
|
|
||||||
patient = relationship('Patient', back_populates='appointments')
|
patient = relationship('Patient', back_populates='appointments')
|
||||||
doctor = relationship('User', 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 sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Backup(BaseModel):
|
class Backup(BaseModel):
|
||||||
__tablename__ = 'backups'
|
__tablename__ = 'backups'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now)
|
||||||
path = Column(String, nullable=False)
|
path = Column(String, nullable=False)
|
||||||
filename = Column(String, nullable=False)
|
filename = Column(String, nullable=False)
|
||||||
is_by_user = Column(Boolean, nullable=False, default=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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class SideEnum(PyEnum):
|
class SideEnum(PyEnum):
|
||||||
@ -13,6 +14,7 @@ class SideEnum(PyEnum):
|
|||||||
|
|
||||||
class Lens(BaseModel):
|
class Lens(BaseModel):
|
||||||
__tablename__ = 'lens'
|
__tablename__ = 'lens'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
tor = Column(Float, nullable=False)
|
tor = Column(Float, nullable=False)
|
||||||
trial = Column(Float, nullable=False)
|
trial = Column(Float, nullable=False)
|
||||||
@ -24,7 +26,7 @@ class Lens(BaseModel):
|
|||||||
side = Column(Enum(SideEnum), nullable=False)
|
side = Column(Enum(SideEnum), nullable=False)
|
||||||
issued = Column(Boolean, nullable=False, default=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')
|
type = relationship('LensType', back_populates='lenses')
|
||||||
|
|
||||||
|
|||||||
@ -2,16 +2,18 @@ from sqlalchemy import Column, Integer, ForeignKey, Date
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class LensIssue(BaseModel):
|
class LensIssue(BaseModel):
|
||||||
__tablename__ = 'lens_issues'
|
__tablename__ = 'lens_issues'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
issue_date = Column(Date, nullable=False)
|
issue_date = Column(Date, nullable=False)
|
||||||
|
|
||||||
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
|
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
|
||||||
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
|
||||||
lens_id = Column(Integer, ForeignKey('lens.id'), nullable=False)
|
lens_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens.id'), nullable=False)
|
||||||
|
|
||||||
patient = relationship('Patient', back_populates='lens_issues')
|
patient = relationship('Patient', back_populates='lens_issues')
|
||||||
doctor = relationship('User', 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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class LensType(BaseModel):
|
class LensType(BaseModel):
|
||||||
__tablename__ = 'lens_types'
|
__tablename__ = 'lens_types'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
title = Column(VARCHAR(150), nullable=False, unique=True)
|
title = Column(VARCHAR(150), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,18 @@ from sqlalchemy.orm import relationship
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Mailing(BaseModel):
|
class Mailing(BaseModel):
|
||||||
__tablename__ = 'mailing'
|
__tablename__ = 'mailing'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
text = Column(String, nullable=False)
|
text = Column(String, nullable=False)
|
||||||
title = Column(String, nullable=False)
|
title = Column(String, nullable=False)
|
||||||
datetime = Column(DateTime, nullable=False, default=func.utcnow)
|
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')
|
user = relationship('User', back_populates='mailing')
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class MailingDeliveryMethod(BaseModel):
|
class MailingDeliveryMethod(BaseModel):
|
||||||
__tablename__ = 'mailing_delivery_methods'
|
__tablename__ = 'mailing_delivery_methods'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
title = Column(VARCHAR(200), nullable=False)
|
title = Column(VARCHAR(200), nullable=False)
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class MailingOption(BaseModel):
|
class MailingOption(BaseModel):
|
||||||
__tablename__ = 'mailing_options'
|
__tablename__ = 'mailing_options'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
option_id = Column(Integer, ForeignKey('mailing_delivery_methods.id'), nullable=False)
|
option_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing_delivery_methods.id'), nullable=False)
|
||||||
mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
|
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False)
|
||||||
|
|
||||||
method = relationship('MailingDeliveryMethod', back_populates='mailing')
|
method = relationship('MailingDeliveryMethod', back_populates='mailing')
|
||||||
mailing = relationship('Mailing', back_populates='mailing_options')
|
mailing = relationship('Mailing', back_populates='mailing_options')
|
||||||
|
|||||||
@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR, Date, String
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Patient(BaseModel):
|
class Patient(BaseModel):
|
||||||
__tablename__ = 'patients'
|
__tablename__ = 'patients'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
first_name = Column(VARCHAR(200), nullable=False)
|
first_name = Column(VARCHAR(200), nullable=False)
|
||||||
last_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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Recipient(BaseModel):
|
class Recipient(BaseModel):
|
||||||
__tablename__ = 'recipients'
|
__tablename__ = 'recipients'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
|
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
|
||||||
mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False)
|
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False)
|
||||||
|
|
||||||
patient = relationship('Patient', back_populates='mailing')
|
patient = relationship('Patient', back_populates='mailing')
|
||||||
mailing = relationship('Mailing', back_populates='recipients')
|
mailing = relationship('Mailing', back_populates='recipients')
|
||||||
|
|||||||
@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Role(BaseModel):
|
class Role(BaseModel):
|
||||||
__tablename__ = 'roles'
|
__tablename__ = 'roles'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
title = Column(VARCHAR(150), nullable=False, unique=True)
|
title = Column(VARCHAR(150), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|||||||
@ -3,17 +3,19 @@ from sqlalchemy.orm import relationship
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class ScheduledAppointment(BaseModel):
|
class ScheduledAppointment(BaseModel):
|
||||||
__tablename__ = 'scheduled_appointments'
|
__tablename__ = 'scheduled_appointments'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now())
|
scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
is_canceled = Column(Boolean, nullable=False, default=False, server_default='false')
|
is_canceled = Column(Boolean, nullable=False, default=False, server_default='false')
|
||||||
|
|
||||||
patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False)
|
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False)
|
||||||
doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False)
|
||||||
type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False)
|
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False)
|
||||||
|
|
||||||
patient = relationship('Patient', back_populates='scheduled_appointments')
|
patient = relationship('Patient', back_populates='scheduled_appointments')
|
||||||
doctor = relationship('User', 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.base import BaseModel
|
||||||
from app.domain.models.lens import SideEnum
|
from app.domain.models.lens import SideEnum
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class SetContent(BaseModel):
|
class SetContent(BaseModel):
|
||||||
__tablename__ = 'set_contents'
|
__tablename__ = 'set_contents'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
tor = Column(Float, nullable=False)
|
tor = Column(Float, nullable=False)
|
||||||
trial = Column(Float, nullable=False)
|
trial = Column(Float, nullable=False)
|
||||||
@ -18,8 +20,8 @@ class SetContent(BaseModel):
|
|||||||
side = Column(Enum(SideEnum), nullable=False)
|
side = Column(Enum(SideEnum), nullable=False)
|
||||||
count = Column(Integer, nullable=False)
|
count = Column(Integer, nullable=False)
|
||||||
|
|
||||||
type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False)
|
type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False)
|
||||||
set_id = Column(Integer, ForeignKey('sets.id'), nullable=False)
|
set_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.sets.id'), nullable=False)
|
||||||
|
|
||||||
type = relationship('LensType', back_populates='contents')
|
type = relationship('LensType', back_populates='contents')
|
||||||
set = relationship('Set', 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 sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class Set(BaseModel):
|
class Set(BaseModel):
|
||||||
__tablename__ = 'sets'
|
__tablename__ = 'sets'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
title = Column(VARCHAR(150), nullable=False, unique=True)
|
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 werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from app.domain.models.base import BaseModel
|
from app.domain.models.base import BaseModel
|
||||||
|
from app.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
|
__table_args__ = {"schema": settings.SCHEMA}
|
||||||
|
|
||||||
first_name = Column(VARCHAR(200), nullable=False)
|
first_name = Column(VARCHAR(200), nullable=False)
|
||||||
last_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)
|
password = Column(String, nullable=False)
|
||||||
is_blocked = Column(Boolean, nullable=False, default=False, server_default='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')
|
role = relationship('Role', back_populates='users')
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import io
|
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
import tarfile
|
|
||||||
import datetime
|
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
|
import aiofiles
|
||||||
from fastapi import HTTPException, UploadFile
|
from fastapi import HTTPException, UploadFile
|
||||||
from magic import magic
|
from magic import magic
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@ -69,6 +71,58 @@ class BackupService:
|
|||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
raise HTTPException(500, f"Ошибка создания бэкапа: {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:
|
async def get_backup_file_by_id(self, backup_id: int) -> FileResponse:
|
||||||
backup = await self.backup_repository.get_by_id(backup_id)
|
backup = await self.backup_repository.get_by_id(backup_id)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi_maintenance import MaintenanceModeMiddleware
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.controllers.appointment_files_router import router as appointment_files_router
|
from app.controllers.appointment_files_router import router as appointment_files_router
|
||||||
@ -29,6 +30,7 @@ def start_app():
|
|||||||
allow_methods=['*'],
|
allow_methods=['*'],
|
||||||
allow_headers=['*'],
|
allow_headers=['*'],
|
||||||
)
|
)
|
||||||
|
# api_app.add_middleware(MaintenanceModeMiddleware, enable_maintenance=False)
|
||||||
|
|
||||||
api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files',
|
api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files',
|
||||||
tags=['appointment_files'])
|
tags=['appointment_files'])
|
||||||
|
|||||||
@ -12,6 +12,7 @@ class Settings(BaseSettings):
|
|||||||
BACKUP_DIR: str = 'backups'
|
BACKUP_DIR: str = 'backups'
|
||||||
BACKUP_DB_URL: str
|
BACKUP_DB_URL: str
|
||||||
PG_DUMP_PATH: str
|
PG_DUMP_PATH: str
|
||||||
|
SCHEMA: str = 'public'
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = '.env'
|
env_file = '.env'
|
||||||
|
|||||||
@ -11,4 +11,5 @@ Werkzeug==3.1.3
|
|||||||
pyjwt==2.10.1
|
pyjwt==2.10.1
|
||||||
python-magic==0.4.27
|
python-magic==0.4.27
|
||||||
aiofiles==24.1.0
|
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'],
|
invalidatesTags: ['Backup'],
|
||||||
}),
|
}),
|
||||||
|
restoreBackup: builder.mutation({
|
||||||
|
query: (backupId) => ({
|
||||||
|
url: `/backups/${backupId}/restore/`,
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,4 +54,5 @@ export const {
|
|||||||
useCreateBackupMutation,
|
useCreateBackupMutation,
|
||||||
useDeleteBackupMutation,
|
useDeleteBackupMutation,
|
||||||
useUploadBackupMutation,
|
useUploadBackupMutation,
|
||||||
|
useRestoreBackupMutation,
|
||||||
} = backupsApi;
|
} = backupsApi;
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import { setSelectedAppointment } from "../../../Redux/Slices/appointmentsSlice.js";
|
import {setSelectedAppointment} from "../../../Redux/Slices/appointmentsSlice.js";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import { useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation } from "../../../Api/appointmentFilesApi.js";
|
import {useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js";
|
||||||
import { baseQueryWithAuth } from "../../../Api/baseQuery.js";
|
import {notification} from "antd";
|
||||||
import { notification } from "antd";
|
import CONFIG from "../../../Core/сonfig.js";
|
||||||
|
|
||||||
const useAppointmentView = () => {
|
const useAppointmentView = () => {
|
||||||
const dispatch = useDispatch();
|
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,
|
selectedAppointment?.id,
|
||||||
{ skip: !selectedAppointment?.id }
|
{skip: !selectedAppointment?.id}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [deleteAppointmentFile, { isLoading: isDeletingFile }] = useDeleteAppointmentFileMutation();
|
const [deleteAppointmentFile, {isLoading: isDeletingFile}] = useDeleteAppointmentFileMutation();
|
||||||
|
|
||||||
const [downloadingFiles, setDownloadingFiles] = useState({});
|
const [downloadingFiles, setDownloadingFiles] = useState({});
|
||||||
const [deletingFiles, setDeletingFiles] = useState({});
|
const [deletingFiles, setDeletingFiles] = useState({});
|
||||||
|
|
||||||
const modalWidth = 700;
|
const modalWidth = 700;
|
||||||
const blockStyle = { marginBottom: 16 };
|
const blockStyle = {marginBottom: 16};
|
||||||
const footerRowStyle = { marginTop: 16 };
|
const footerRowStyle = {marginTop: 16};
|
||||||
const footerButtonStyle = { marginRight: 8 };
|
const footerButtonStyle = {marginRight: 8};
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
title: "Просмотр приема",
|
title: "Просмотр приема",
|
||||||
@ -81,54 +81,84 @@ const useAppointmentView = () => {
|
|||||||
const downloadFile = async (fileId, fileName) => {
|
const downloadFile = async (fileId, fileName) => {
|
||||||
try {
|
try {
|
||||||
setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
|
setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
|
||||||
const { url, ...options } = await baseQueryWithAuth(
|
|
||||||
{
|
|
||||||
url: `/appointment_files/${fileId}/file/`,
|
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include',
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const token = localStorage.getItem('access_token');
|
||||||
...options,
|
if (!token) {
|
||||||
method: 'GET',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка при скачивании файла",
|
message: "Ошибка",
|
||||||
description: "Не удалось загрузить файл.",
|
description: "Токен не найден",
|
||||||
placement: "topRight",
|
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 blob = await response.blob();
|
||||||
const downloadUrl = window.URL.createObjectURL(blob);
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
link.setAttribute("download", fileName || "file");
|
link.setAttribute("download", safeFileName);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
window.URL.revokeObjectURL(downloadUrl);
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error downloading file:", error);
|
console.error("Error downloading file:", error); // Отладка
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка при скачивании файлов",
|
message: "Ошибка",
|
||||||
description: "Не удалось загрузить файл.",
|
description: error.message || "Не удалось скачать файл",
|
||||||
placement: "topRight",
|
placement: "topRight",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setDownloadingFiles((prev) => ({ ...prev, [fileId]: false }));
|
setDownloadingFiles((prev) => ({...prev, [fileId]: false}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFile = async (fileId, fileName) => {
|
const deleteFile = async (fileId, fileName) => {
|
||||||
try {
|
try {
|
||||||
setDeletingFiles((prev) => ({ ...prev, [fileId]: true }));
|
setDeletingFiles((prev) => ({...prev, [fileId]: true}));
|
||||||
await deleteAppointmentFile(fileId).unwrap();
|
await deleteAppointmentFile(fileId).unwrap();
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "Файл удален",
|
message: "Файл удален",
|
||||||
@ -143,7 +173,7 @@ const useAppointmentView = () => {
|
|||||||
placement: "topRight",
|
placement: "topRight",
|
||||||
});
|
});
|
||||||
} finally {
|
} 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 {Button, Divider, List, Popconfirm, Result, Space, Tooltip, Typography, Upload} from "antd";
|
||||||
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} from "@ant-design/icons";
|
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined} from "@ant-design/icons";
|
||||||
import useBackupManageTab from "./useBackupManageTab.js";
|
import useBackupManageTab from "./useBackupManageTab.js";
|
||||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ const BackupManageTab = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (backupManageTabData.isErrorBackups) {
|
if (backupManageTabData.isErrorBackups) {
|
||||||
return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>
|
return <Result status={500} title="Произошла ошибка при загрузке резервных копий"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -22,7 +22,6 @@ const BackupManageTab = () => {
|
|||||||
icon={<CloudDownloadOutlined/>}
|
icon={<CloudDownloadOutlined/>}
|
||||||
onClick={backupManageTabData.createBackupHandler}
|
onClick={backupManageTabData.createBackupHandler}
|
||||||
loading={backupManageTabData.isCreatingBackup}
|
loading={backupManageTabData.isCreatingBackup}
|
||||||
|
|
||||||
>
|
>
|
||||||
Создать резервную копию
|
Создать резервную копию
|
||||||
</Button>
|
</Button>
|
||||||
@ -46,33 +45,47 @@ const BackupManageTab = () => {
|
|||||||
renderItem={(backup) => (
|
renderItem={(backup) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Typography.Text>{backup.filename}</Typography.Text>
|
<Typography.Text>{backup.filename}</Typography.Text>
|
||||||
|
<Divider type={"vertical"}/>
|
||||||
<Typography.Text>{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}</Typography.Text>
|
<Typography.Text>{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}</Typography.Text>
|
||||||
<Tooltip title="Скачать резервную копию">
|
<Space>
|
||||||
<Button
|
<Tooltip title="Скачать резервную копию">
|
||||||
type="primary"
|
<Button
|
||||||
icon={<CloudDownloadOutlined/>}
|
type="primary"
|
||||||
onClick={() => backupManageTabData.downloadBackupHandler(backup.id, backup.filename)}
|
icon={<CloudDownloadOutlined/>}
|
||||||
loading={backupManageTabData.isDownloadingBackup}
|
onClick={() => backupManageTabData.downloadBackupHandler(backup.id, backup.filename)}
|
||||||
>
|
loading={backupManageTabData.isDownloadingBackup}
|
||||||
Скачать
|
>
|
||||||
</Button>
|
Скачать
|
||||||
</Tooltip>
|
</Button>
|
||||||
<Tooltip title="Удалить резервную копию">
|
</Tooltip>
|
||||||
<Button
|
<Tooltip title="Восстановить резервную копию">
|
||||||
type="primary"
|
<Popconfirm title={"Вы действительно хотите восстановить резервную копию? Существующие данные будут утеряны навсегда."}
|
||||||
icon={<DeleteOutlined/>}
|
onConfirm={() => backupManageTabData.restoreBackupHandler(backup.id)}>
|
||||||
onClick={() => backupManageTabData.deleteBackupHandler(backup.id)}
|
<Button
|
||||||
loading={backupManageTabData.isDeletingBackup}
|
type="primary"
|
||||||
danger
|
icon={<ReloadOutlined/>}
|
||||||
>
|
loading={backupManageTabData.isRestoringBackup}
|
||||||
Удалить
|
>
|
||||||
</Button>
|
Восстановить
|
||||||
</Tooltip>
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Удалить резервную копию">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<DeleteOutlined/>}
|
||||||
|
onClick={() => backupManageTabData.deleteBackupHandler(backup.id)}
|
||||||
|
loading={backupManageTabData.isDeletingBackup}
|
||||||
|
danger
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
useCreateBackupMutation,
|
useCreateBackupMutation,
|
||||||
useDeleteBackupMutation,
|
useDeleteBackupMutation,
|
||||||
useGetBackupsQuery, useUploadBackupMutation
|
useGetBackupsQuery,
|
||||||
|
useUploadBackupMutation,
|
||||||
|
useRestoreBackupMutation,
|
||||||
} from "../../../../../Api/backupsApi.js";
|
} from "../../../../../Api/backupsApi.js";
|
||||||
import {useDispatch} from "react-redux";
|
import {useDispatch} from "react-redux";
|
||||||
import {notification} from "antd";
|
import {notification} from "antd";
|
||||||
@ -10,13 +12,13 @@ import CONFIG from "../../../../../Core/сonfig.js";
|
|||||||
|
|
||||||
|
|
||||||
const useBackupManageTab = () => {
|
const useBackupManageTab = () => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, {
|
const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, {
|
||||||
pollingInterval: 60000,
|
pollingInterval: 60000,
|
||||||
});
|
});
|
||||||
const [createBackup, {isLoading: isCreatingBackup}] = useCreateBackupMutation();
|
const [createBackup, {isLoading: isCreatingBackup}] = useCreateBackupMutation();
|
||||||
const [deleteBackup, {isLoading: isDeletingBackup}] = useDeleteBackupMutation();
|
const [deleteBackup, {isLoading: isDeletingBackup}] = useDeleteBackupMutation();
|
||||||
const [uploadBackup, {isLoading: isUploadingBackup}] = useUploadBackupMutation();
|
const [uploadBackup, {isLoading: isUploadingBackup}] = useUploadBackupMutation();
|
||||||
|
const [restoreBackup, {isLoading: isRestoringBackup}] = useRestoreBackupMutation();
|
||||||
|
|
||||||
const [isDownloadingBackup, setDownloadingFiles] = useState(false);
|
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) => {
|
const deleteBackupHandler = async (backupId) => {
|
||||||
try {
|
try {
|
||||||
@ -142,6 +158,7 @@ const useBackupManageTab = () => {
|
|||||||
placement: "topRight",
|
placement: "topRight",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error("Upload error:", e); // Отладка
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Ошибка",
|
message: "Ошибка",
|
||||||
description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию",
|
description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию",
|
||||||
@ -158,11 +175,13 @@ const useBackupManageTab = () => {
|
|||||||
isDownloadingBackup,
|
isDownloadingBackup,
|
||||||
isDeletingBackup,
|
isDeletingBackup,
|
||||||
isUploadingBackup,
|
isUploadingBackup,
|
||||||
|
isRestoringBackup,
|
||||||
createBackupHandler,
|
createBackupHandler,
|
||||||
downloadBackupHandler,
|
downloadBackupHandler,
|
||||||
|
restoreBackupHandler,
|
||||||
deleteBackupHandler,
|
deleteBackupHandler,
|
||||||
uploadBackupHandler,
|
uploadBackupHandler,
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useBackupManageTab;
|
export default useBackupManageTab;
|
||||||
Loading…
x
Reference in New Issue
Block a user