feat: Бэкапы: Удалено восстановление бэкапов.
This commit is contained in:
parent
8042460557
commit
89bfb07a23
@ -79,27 +79,6 @@ 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,
|
||||||
|
|||||||
@ -71,58 +71,6 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@ -40,12 +40,6 @@ export const backupsApi = createApi({
|
|||||||
},
|
},
|
||||||
invalidatesTags: ['Backup'],
|
invalidatesTags: ['Backup'],
|
||||||
}),
|
}),
|
||||||
restoreBackup: builder.mutation({
|
|
||||||
query: (backupId) => ({
|
|
||||||
url: `/backups/${backupId}/restore/`,
|
|
||||||
method: 'POST',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,5 +48,4 @@ export const {
|
|||||||
useCreateBackupMutation,
|
useCreateBackupMutation,
|
||||||
useDeleteBackupMutation,
|
useDeleteBackupMutation,
|
||||||
useUploadBackupMutation,
|
useUploadBackupMutation,
|
||||||
useRestoreBackupMutation,
|
|
||||||
} = backupsApi;
|
} = backupsApi;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {Button, Divider, List, Popconfirm, Result, Space, Tooltip, Typography, Upload} from "antd";
|
import {Button, Divider, List, Result, Space, Tooltip, Typography, Upload} from "antd";
|
||||||
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined} from "@ant-design/icons";
|
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} 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";
|
||||||
|
|
||||||
@ -58,18 +58,6 @@ const BackupManageTab = () => {
|
|||||||
Скачать
|
Скачать
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Восстановить резервную копию">
|
|
||||||
<Popconfirm title={"Вы действительно хотите восстановить резервную копию? Существующие данные будут утеряны навсегда."}
|
|
||||||
onConfirm={() => backupManageTabData.restoreBackupHandler(backup.id)}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<ReloadOutlined/>}
|
|
||||||
loading={backupManageTabData.isRestoringBackup}
|
|
||||||
>
|
|
||||||
Восстановить
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Удалить резервную копию">
|
<Tooltip title="Удалить резервную копию">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
useUploadBackupMutation,
|
useUploadBackupMutation,
|
||||||
useRestoreBackupMutation,
|
useRestoreBackupMutation,
|
||||||
} from "../../../../../Api/backupsApi.js";
|
} from "../../../../../Api/backupsApi.js";
|
||||||
import {useDispatch} from "react-redux";
|
|
||||||
import {notification} from "antd";
|
import {notification} from "antd";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import CONFIG from "../../../../../Core/сonfig.js";
|
import CONFIG from "../../../../../Core/сonfig.js";
|
||||||
@ -18,7 +17,6 @@ const useBackupManageTab = () => {
|
|||||||
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);
|
||||||
|
|
||||||
@ -109,21 +107,6 @@ 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 {
|
||||||
await deleteBackup(backupId).unwrap();
|
await deleteBackup(backupId).unwrap();
|
||||||
@ -175,10 +158,8 @@ const useBackupManageTab = () => {
|
|||||||
isDownloadingBackup,
|
isDownloadingBackup,
|
||||||
isDeletingBackup,
|
isDeletingBackup,
|
||||||
isUploadingBackup,
|
isUploadingBackup,
|
||||||
isRestoringBackup,
|
|
||||||
createBackupHandler,
|
createBackupHandler,
|
||||||
downloadBackupHandler,
|
downloadBackupHandler,
|
||||||
restoreBackupHandler,
|
|
||||||
deleteBackupHandler,
|
deleteBackupHandler,
|
||||||
uploadBackupHandler,
|
uploadBackupHandler,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user