feat: Бэкапы и загрузка бэкапов админом

This commit is contained in:
Андрей Дувакин 2025-07-02 10:38:07 +05:00
parent c70b109851
commit b52c0d16fa
5 changed files with 15 additions and 11 deletions

View File

@ -71,7 +71,11 @@ async def upload_backup(
db: AsyncSession = Depends(get_db),
user=Depends(require_admin),
):
backup_service = BackupService(db)
backup_service = BackupService(
db,
backup_dir=settings.BACKUP_DIR,
app_files_dir=settings.FILE_UPLOAD_DIR,
)
return await backup_service.upload_backup(file, user)

View File

@ -89,8 +89,7 @@ class BackupService:
file.file.seek(0)
self.validate_file_type(file)
if not self.validate_backup_archive(file_bytes,
expected_app_files_dir_name=os.path.basename(self.app_files_dir)):
if not self.validate_backup_archive(file_bytes, self.app_files_dir):
raise HTTPException(400, "Неверная структура архива резервной копии")
filename = self.generate_filename(file)
@ -102,6 +101,7 @@ class BackupService:
filename=filename,
path=backup_path,
user_id=user.id,
is_by_user=True,
)
await self.backup_repository.create(backup_record)
return self.model_to_entity(backup_record)
@ -132,12 +132,15 @@ class BackupService:
try:
with tarfile.open(fileobj=io.BytesIO(file_bytes), mode="r:gz") as tar:
members = tar.getnames()
if "db_dump.sql" not in members:
return False
if not any(name.startswith(expected_app_files_dir_name) for name in members):
return False
return True
except Exception:
except Exception as e:
return False
@staticmethod
@ -156,9 +159,7 @@ class BackupService:
mime = magic.Magic(mime=True)
file_type = mime.from_buffer(file.file.read(1024))
file.file.seek(0)
content = file.file.read()
print(content.decode('utf-8'))
if file_type not in ["application/zip", "application/gzip"]:
if file_type not in ["application/zip", "application/gzip", "application/x-gzip"]:
raise HTTPException(400, "Неправильный формат файла")
@staticmethod

View File

@ -9,8 +9,7 @@ export const baseQuery = fetchBaseQuery({
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
if (endpoint === 'uploadAppointmentFile') {
if (endpoint === 'uploadAppointmentFile' || endpoint === 'uploadBackup') {
const mutation = getState()?.api?.mutations?.[Object.keys(getState()?.api?.mutations || {})[0]];
if (mutation?.body instanceof FormData) {
headers.delete('Content-Type');

View File

@ -202,7 +202,7 @@ const AppointmentFormModal = () => {
fileList={appointmentFormModalUI.draftFiles}
beforeUpload={(file) => {
appointmentFormModalUI.handleAddFile(file);
return false; // Prevent auto-upload
return false;
}}
onRemove={(file) => appointmentFormModalUI.handleRemoveFile(file)}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"

View File

@ -46,7 +46,7 @@ const BackupManageTab = () => {
renderItem={(backup) => (
<List.Item>
<Typography.Text>{backup.filename}</Typography.Text>
<Typography.Text>Создан: {new Date(backup.timestamp).toLocaleString()}</Typography.Text>
<Typography.Text>{backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()}</Typography.Text>
<Tooltip title="Скачать резервную копию">
<Button
type="primary"