feat: Бэкапы: Удалено восстановление бэкапов.

This commit is contained in:
Андрей Дувакин 2025-07-03 08:45:22 +05:00
parent 8042460557
commit 89bfb07a23
5 changed files with 2 additions and 113 deletions

View File

@ -79,27 +79,6 @@ 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

@ -71,58 +71,6 @@ 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

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

View File

@ -1,5 +1,5 @@
import {Button, Divider, List, Popconfirm, Result, Space, Tooltip, Typography, Upload} from "antd";
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined} from "@ant-design/icons";
import {Button, Divider, List, Result, Space, Tooltip, Typography, Upload} from "antd";
import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} from "@ant-design/icons";
import useBackupManageTab from "./useBackupManageTab.js";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
@ -58,18 +58,6 @@ const BackupManageTab = () => {
Скачать
</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"

View File

@ -5,7 +5,6 @@ import {
useUploadBackupMutation,
useRestoreBackupMutation,
} from "../../../../../Api/backupsApi.js";
import {useDispatch} from "react-redux";
import {notification} from "antd";
import {useState} from "react";
import CONFIG from "../../../../../Core/сonfig.js";
@ -18,7 +17,6 @@ const useBackupManageTab = () => {
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);
@ -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) => {
try {
await deleteBackup(backupId).unwrap();
@ -175,10 +158,8 @@ const useBackupManageTab = () => {
isDownloadingBackup,
isDeletingBackup,
isUploadingBackup,
isRestoringBackup,
createBackupHandler,
downloadBackupHandler,
restoreBackupHandler,
deleteBackupHandler,
uploadBackupHandler,
};