+
+
- {{ t('components.terminal.machineFileUpload.totalSize') }}
+ {{ $t('components.terminal.machineFileUpload.totalSize') }}
+
+ {{ formatByteSize(progress.totalSize) }}
+
-
- {{ formatByteSize(progress.totalSize) }}
+
+
+
+ {{ $t('components.terminal.machineFileUpload.uploaded') }}
+
+ {{ formatByteSize(progress.uploadedSize) }}
+
-
-
+
- {{ t('components.terminal.machineFileUpload.speed') }}
-
-
- {{ speed }}
+ {{ $t('components.terminal.machineFileUpload.speed') }}
+
+ {{ speed }}
+
@@ -62,7 +65,6 @@
diff --git a/frontend/src/components/sysmsg/machine/MachineFolderUploadProgress.vue b/frontend/src/components/sysmsg/machine/MachineFolderUploadProgress.vue
index 58151fa2..fb305bf5 100644
--- a/frontend/src/components/sysmsg/machine/MachineFolderUploadProgress.vue
+++ b/frontend/src/components/sysmsg/machine/MachineFolderUploadProgress.vue
@@ -13,12 +13,18 @@
- {{ progress.folderName }}
- {{ progress.uploadedFiles }}/{{ progress.totalFiles }}
+ {{
+ progress.folderName
+ }}
+ {{ progress.uploadedFiles }}/{{ progress.totalFiles }}
+
+
+ {{ $t('common.cancel') }}
+
-
+
@@ -28,37 +34,43 @@
-
{{ t('machine.uploading') }} ({{ t('machine.concurrentFiles', { count: progress.uploadingFiles.length }) }}):
+
+ {{ $t('machine.uploading') }} ({{ $t('machine.concurrentFiles', { count: progress.uploadingFiles.length }) }}):
+
{{ file }}
-
-
-
-
- {{ progress.lastFile }}
-
diff --git a/frontend/src/components/sysmsg/machine/machine-file-upload-progress.ts b/frontend/src/components/sysmsg/machine/machine-file-upload-progress.ts
index 1110fc42..e8638275 100644
--- a/frontend/src/components/sysmsg/machine/machine-file-upload-progress.ts
+++ b/frontend/src/components/sysmsg/machine/machine-file-upload-progress.ts
@@ -1,27 +1,13 @@
import syssocket from '@/common/syssocket';
-import { reactive, h } from 'vue';
-import { ElNotification } from 'element-plus';
+import { createOrUpdateNotification, completeNotification, activeNotifications } from '../global-notification-manager';
import MachineFileUploadProgress from './MachineFileUploadProgress.vue';
+import { reactive, nextTick } from 'vue';
-// 文件上传进度通知映射表(key: uploadId, value: 通知实例)
-const fileUploadNotifyMap: Map
= new Map();
+// 存储上传任务的取消方法
+const uploadAborters = new Map void; progress?: any }>();
-/**
- * 构建机器文件上传进度组件属性
- */
-const buildMachineFileUploadProgressProps = (): any => {
- return {
- progress: reactive({
- authCertName: '', // 授权凭证名
- path: '', // 文件路径
- filename: '',
- uploadedSize: 0,
- totalSize: 0,
- timestamp: 0,
- status: '', // '' | 'success' | 'exception'
- }),
- };
-};
+// 存储待注册的 abort 方法(等待 WebSocket 消息到达)
+const pendingAborters = new Map void>();
/**
* 注册机器文件上传进度消息处理
@@ -31,77 +17,79 @@ export async function registerMachineFileUploadProgress() {
const content = message.params;
const uploadId = content.uploadId;
- // 上传完成或失败,关闭通知
+ // 上传完成或失败
if (content.status === 'complete' || content.status === 'error') {
- const notify = fileUploadNotifyMap.get(uploadId);
-
- if (notify && notify.notification) {
- // 更新最终状态
- notify.props.progress.status = content.status === 'complete' ? 'success' : 'exception';
- notify.props.progress.percent = content.status === 'complete' ? 100 : notify.props.progress.percent;
-
- // 强制更新 VNode
- try {
- if (notify.notification.state) {
- notify.notification.state.message = h(MachineFileUploadProgress, notify.props);
- } else if (notify.notification.vm) {
- notify.notification.vm.exposed?.message?.(h(MachineFileUploadProgress, notify.props));
- }
- } catch (e) {
- console.warn('[MachineFileUpload] Failed to update notification VNode:', e);
- }
-
- // 1秒后关闭通知
- setTimeout(() => {
- if (notify.notification) {
- notify.notification.close();
- }
- fileUploadNotifyMap.delete(uploadId);
- }, 1000);
- }
+ completeNotification(uploadId, 1000);
+ uploadAborters.delete(uploadId);
return;
}
- // 获取或创建通知
- let notify = fileUploadNotifyMap.get(uploadId);
- if (!notify) {
- notify = {
- props: buildMachineFileUploadProgressProps(),
- notification: undefined,
- };
- fileUploadNotifyMap.set(uploadId, notify);
- }
+ // 构建组件props
+ const props = {
+ progress: reactive({
+ authCertName: content.authCertName || '',
+ path: content.path || '',
+ filename: content.filename || '',
+ uploadedSize: content.uploadedSize || 0,
+ totalSize: content.totalSize || 0,
+ timestamp: content.timestamp || 0,
+ status: content.status || 'uploading',
+ }),
+ onCancel: () => {
+ const aborter = uploadAborters.get(uploadId);
+ if (aborter) {
+ aborter.abort();
- // 更新进度
- notify.props.progress.authCertName = content.authCertName || '';
- notify.props.progress.path = content.path || '';
- notify.props.progress.filename = content.filename || notify.props.progress.filename;
- notify.props.progress.uploadedSize = content.uploadedSize || 0;
- notify.props.progress.totalSize = content.totalSize || 0;
- notify.props.progress.timestamp = content.timestamp || 0;
- notify.props.progress.status = 'uploading';
+ // 更新通知状态为取消
+ if (aborter.progress) {
+ nextTick(() => {
+ aborter.progress.status = 'error';
+ aborter.progress.filename = '已取消: ' + (aborter.progress.filename || '');
+ });
- // 首次创建通知
- if (!notify.notification) {
- notify.notification = ElNotification({
- duration: 0,
- title: message.title || '机器文件上传',
- message: h(MachineFileUploadProgress, notify.props),
- showClose: true,
- offset: 60,
- customClass: 'machine-file-upload-notification',
- });
- } else {
- // 已存在通知,强制更新 message
- try {
- if (notify.notification.state) {
- notify.notification.state.message = h(MachineFileUploadProgress, notify.props);
- } else if (notify.notification.vm) {
- notify.notification.vm.exposed?.message?.(h(MachineFileUploadProgress, notify.props));
+ // 延迟后关闭通知
+ setTimeout(() => {
+ completeNotification(uploadId, 1000);
+ uploadAborters.delete(uploadId);
+ }, 1500);
+ } else {
+ uploadAborters.delete(uploadId);
+ }
}
- } catch (e) {
- console.warn('[MachineFileUpload] Failed to update notification VNode:', e);
- }
+ },
+ };
+
+ // 创建或更新上传通知
+ createOrUpdateNotification(uploadId, 'machineFileUpload', content, MachineFileUploadProgress, props, {
+ title: message.title || 'machine.fileUpload',
+ onCancel: props.onCancel,
+ });
+
+ // 如果有待注册的 abort 方法,现在注册
+ const pendingAbort = pendingAborters.get(uploadId);
+ if (pendingAbort) {
+ console.log('[MachineFileUpload] Registering pending aborter for uploadId:', uploadId);
+ uploadAborters.set(uploadId, { abort: pendingAbort, progress: props.progress });
+ pendingAborters.delete(uploadId);
}
});
}
+
+/**
+ * 注册上传任务的取消方法
+ * @param uploadId 上传ID
+ * @param abort 取消方法
+ */
+export function registerUploadAborter(uploadId: string, abort: () => void) {
+ // 先检查通知是否已经存在
+ const task = activeNotifications.get(uploadId);
+ const progress = task?.componentProps?.progress || null;
+
+ if (progress) {
+ // 通知已存在,直接注册
+ uploadAborters.set(uploadId, { abort, progress });
+ } else {
+ // 通知还未创建,保存为 pending
+ pendingAborters.set(uploadId, abort);
+ }
+}
diff --git a/frontend/src/components/sysmsg/machine/machine-folder-upload-progress.ts b/frontend/src/components/sysmsg/machine/machine-folder-upload-progress.ts
index 5a8738a8..68d891ec 100644
--- a/frontend/src/components/sysmsg/machine/machine-folder-upload-progress.ts
+++ b/frontend/src/components/sysmsg/machine/machine-folder-upload-progress.ts
@@ -1,31 +1,13 @@
-import { ElNotification } from 'element-plus';
-import { h, reactive } from 'vue';
-import MachineFolderUploadProgress from '@/components/sysmsg/machine/MachineFolderUploadProgress.vue';
import syssocket from '@/common/syssocket';
+import { createOrUpdateNotification, completeNotification, activeNotifications } from '../global-notification-manager';
+import MachineFolderUploadProgress from './MachineFolderUploadProgress.vue';
+import { reactive, nextTick } from 'vue';
-// 文件夹上传通知 Map
-const folderUploadNotifyMap = new Map();
+// 存储上传任务的取消方法
+const folderUploadAborters = new Map void; progress?: any }>();
-/**
- * 构建文件夹上传进度组件的 props
- */
-const buildMachineFolderUploadProgressProps = (): any => {
- return {
- progress: reactive({
- authCertName: '', // 授权凭证名
- path: '', // 文件路径
- folderName: '',
- totalFiles: 0,
- uploadedFiles: 0,
- totalSize: 0,
- uploadedSize: 0,
- lastFile: '',
- uploadingFiles: [] as string[],
- timestamp: 0,
- status: '',
- }),
- };
-};
+// 存储待注册的 abort 方法(等待 WebSocket 消息到达)
+const pendingFolderAborters = new Map void>();
/**
* 注册文件夹上传进度消息处理
@@ -39,81 +21,83 @@ export async function registerFolderUploadProgressHandler() {
return;
}
- // 获取或创建通知
- let notify = folderUploadNotifyMap.get(uploadId);
-
- if (!notify) {
- // 首次创建通知
- const props = buildMachineFolderUploadProgressProps();
-
- const notificationInstance = ElNotification({
- title: '文件夹上传',
- message: h(MachineFolderUploadProgress, props),
- duration: 0,
- position: 'top-right',
- offset: 60,
- customClass: 'machine-folder-upload-notify',
- onClose: () => {
- folderUploadNotifyMap.delete(uploadId);
- },
- });
-
- notify = {
- props,
- notification: notificationInstance,
- };
-
- folderUploadNotifyMap.set(uploadId, notify);
- }
-
- // 上传完成或失败,关闭通知
+ // 上传完成或失败
if (content.status === 'complete' || content.status === 'error') {
- if (notify && notify.notification) {
- // 更新最终状态
- notify.props.progress.status = content.status === 'complete' ? 'success' : 'exception';
-
- // 强制更新 VNode
- try {
- if (notify.notification.state) {
- notify.notification.state.message = h(MachineFolderUploadProgress, notify.props);
- }
- } catch (e) {
- console.warn('[MachineFolderUpload] Failed to update notification VNode:', e);
- }
-
- // 1秒后关闭通知
- setTimeout(() => {
- if (notify.notification) {
- notify.notification.close();
- }
- folderUploadNotifyMap.delete(uploadId);
- }, 1000);
- }
+ completeNotification(uploadId, 1000);
+ folderUploadAborters.delete(uploadId);
return;
}
- // 更新进度
- if (content.status === 'uploading') {
- notify.props.progress.authCertName = content.authCertName || '';
- notify.props.progress.path = content.path || '';
- notify.props.progress.folderName = content.folderName || '';
- notify.props.progress.totalFiles = content.totalFiles || 0;
- notify.props.progress.uploadedFiles = content.uploadedFiles || 0;
- notify.props.progress.totalSize = content.totalSize || 0;
- notify.props.progress.uploadedSize = content.uploadedSize || 0;
- notify.props.progress.lastFile = content.lastFile || '';
- notify.props.progress.uploadingFiles = content.uploadingFiles || [];
- notify.props.progress.timestamp = content.timestamp || 0;
- notify.props.progress.status = 'uploading';
+ // 构建组件props
+ const props = {
+ progress: reactive({
+ authCertName: content.authCertName || '',
+ path: content.path || '',
+ folderName: content.folderName || '',
+ totalFiles: content.totalFiles || 0,
+ uploadedFiles: content.uploadedFiles || 0,
+ totalSize: content.totalSize || 0,
+ uploadedSize: content.uploadedSize || 0,
+ uploadingFiles: content.uploadingFiles || [],
+ timestamp: content.timestamp || 0,
+ status: content.status || 'uploading',
+ }),
+ onCancel: () => {
+ const aborter = folderUploadAborters.get(uploadId);
+ if (aborter) {
+ aborter.abort();
- // 强制更新 VNode
- try {
- if (notify.notification && notify.notification.state) {
- notify.notification.state.message = h(MachineFolderUploadProgress, notify.props);
+ // 更新通知状态为取消
+ if (aborter.progress) {
+ nextTick(() => {
+ aborter.progress.status = 'exception';
+ aborter.progress.folderName = '已取消: ' + (aborter.progress.folderName || '');
+ });
+
+ // 延迟后关闭通知
+ setTimeout(() => {
+ completeNotification(uploadId, 1000);
+ folderUploadAborters.delete(uploadId);
+ }, 1500);
+ } else {
+ folderUploadAborters.delete(uploadId);
+ }
}
- } catch (e) {
- console.warn('[MachineFolderUpload] Failed to update notification VNode:', e);
- }
+ },
+ };
+
+ // 创建或更新上传通知
+ if (content.status === 'uploading') {
+ createOrUpdateNotification(uploadId, 'machineFolderUpload', content, MachineFolderUploadProgress, props, {
+ title: message.title || 'machine.folderUpload',
+ onCancel: props.onCancel,
+ });
+ }
+
+ // 如果有待注册的 abort 方法,现在注册
+ const pendingAbort = pendingFolderAborters.get(uploadId);
+ if (pendingAbort) {
+ folderUploadAborters.set(uploadId, { abort: pendingAbort, progress: props.progress });
+ pendingFolderAborters.delete(uploadId);
}
});
}
+
+/**
+ * 注册文件夹上传任务的取消方法
+ * @param uploadId 上传ID
+ * @param abort 取消方法
+ */
+export function registerFolderUploadAborter(uploadId: string, abort: () => void) {
+ // 先检查通知是否已经存在
+ const task = activeNotifications.get(uploadId);
+ const progress = task?.componentProps?.progress || null;
+
+ if (progress) {
+ // 通知已存在,直接注册
+ folderUploadAborters.set(uploadId, { abort, progress });
+ } else {
+ // 通知还未创建,保存为 pending
+ pendingFolderAborters.set(uploadId, abort);
+ }
+}
diff --git a/frontend/src/components/terminal/TerminalBody.vue b/frontend/src/components/terminal/TerminalBody.vue
index 5ca3dbbd..6029cb87 100644
--- a/frontend/src/components/terminal/TerminalBody.vue
+++ b/frontend/src/components/terminal/TerminalBody.vue
@@ -19,7 +19,10 @@ import '@xterm/xterm/css/xterm.css';
import config from '@/common/config';
import { createWebSocket, joinClientParams } from '@/common/request';
import { downloadFile } from '@/common/utils/file';
+import { copyToClipboard } from '@/common/utils/string';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
+import { registerUploadAborter } from '@/components/sysmsg/machine/machine-file-upload-progress';
+import { registerFolderUploadAborter } from '@/components/sysmsg/machine/machine-folder-upload-progress';
import { useThemeConfig } from '@/store/themeConfig';
import { machineApi, uploadFile, uploadFolder } from '@/views/ops/machine/api';
import { useDebounceFn, useEventListener } from '@vueuse/core';
@@ -367,11 +370,6 @@ const setupContextMenu = () => {
terminalRef.value.addEventListener('contextmenu', async (event: MouseEvent) => {
event.preventDefault();
- // 如果没有 machineId,不显示文件传输菜单
- if (!props.machineId || !props.authCertName) {
- return; // 直接返回,不显示任何菜单
- }
-
showContextMenu(event, term.getSelection());
});
};
@@ -386,26 +384,41 @@ const showContextMenu = (event: MouseEvent, selectedText: string) => {
// 始终添加上传文件和上传文件夹按钮
state.contextmenu.items = [
+ new ContextmenuItem('copy', 'common.copy')
+ .withIcon('CopyDocument')
+ .withHideFunc(() => !selectedText)
+ .withOnClick(() => {
+ copyToClipboard(selectedText);
+ }),
+ new ContextmenuItem('paste', 'common.paste').withIcon('Document').withOnClick(async () => {
+ try {
+ const text = await navigator.clipboard.readText();
+ if (text) {
+ term.paste(text);
+ focus();
+ }
+ } catch (err) {
+ console.log(err);
+ ElMessage.error(t('common.pasteFailed'));
+ }
+ }),
new ContextmenuItem('download', 'components.terminal.downloadSelectedFile')
.withIcon('Download')
.withHideFunc(() => !selectedText)
.withOnClick(() => {
downloadSelectedFile(state.contextmenu.selectedItem);
- contextmenuRef.value?.closeContextmenu();
}),
new ContextmenuItem('uploadFile', 'components.terminal.uploadFileToCurrentDir')
.withIcon('Upload')
- .withHideFunc(() => false)
+ .withHideFunc(() => !props.machineId || !props.authCertName)
.withOnClick(() => {
triggerFilesUpload();
- contextmenuRef.value?.closeContextmenu();
}),
new ContextmenuItem('uploadFolder', 'components.terminal.uploadFolderToCurrentDir')
.withIcon('Upload')
- .withHideFunc(() => false)
+ .withHideFunc(() => !props.machineId || !props.authCertName)
.withOnClick(() => {
triggerFolderUpload();
- contextmenuRef.value?.closeContextmenu();
}),
];
@@ -501,7 +514,7 @@ const uploadFilesToCurrentPath = async (files: FileList) => {
const file = files[0];
// 使用统一的 HTTP 上传方法
- uploadFile(
+ const { uploadId, abort } = uploadFile(
file,
{
machineId: props.machineId as number,
@@ -520,6 +533,9 @@ const uploadFilesToCurrentPath = async (files: FileList) => {
},
}
);
+
+ // 注册取消方法
+ registerUploadAborter(uploadId, abort);
} catch (error: any) {
ElMessage.error(t('components.terminal.uploadFailed', { error: error.message }));
}
@@ -532,7 +548,7 @@ const uploadFolderToCurrentPath = async (files: FileList) => {
const currentPath = await getCurrentPathOrDefault();
// 使用文件夹上传
- uploadFolder(
+ const { uploadId, abort } = uploadFolder(
files,
{
machineId: props.machineId as number,
@@ -550,6 +566,9 @@ const uploadFolderToCurrentPath = async (files: FileList) => {
},
}
);
+
+ // 注册取消方法
+ registerFolderUploadAborter(uploadId, abort);
} catch (error: any) {
ElMessage.error(t('components.terminal.uploadFailed', { error: error.message }));
}
diff --git a/frontend/src/i18n/en/common.ts b/frontend/src/i18n/en/common.ts
index 6849925f..1c4d3916 100644
--- a/frontend/src/i18n/en/common.ts
+++ b/frontend/src/i18n/en/common.ts
@@ -54,6 +54,8 @@ export default {
previousStep: 'Previous Step',
nextStep: 'Next Step',
copy: 'Copy',
+ paste: 'Paste',
+ pasteFailed: 'Paste failed',
copySuccess: 'Copy Success',
copyCell: 'Copy Cell',
@@ -353,5 +355,13 @@ export default {
title: 'please select the icon',
placeholder: 'please enter content search icon or select icon',
},
+
+ // System message notifications
+ sysmsg: {
+ notifications: {
+ title: 'System Notifications',
+ closeAll: 'Close All',
+ },
+ },
},
};
diff --git a/frontend/src/i18n/en/db.ts b/frontend/src/i18n/en/db.ts
index dd3db6b1..4b3efc11 100644
--- a/frontend/src/i18n/en/db.ts
+++ b/frontend/src/i18n/en/db.ts
@@ -225,6 +225,14 @@ export default {
running: 'Running',
waitRun: 'Wait Run',
+
+ // SQL execution
+ sqlExecute: 'SQL Execution',
+ executedStatements: 'Executed',
+ elapsedTime: 'Elapsed',
+ scriptFileUploadSuccess: 'SQL file [{filename}] executed successfully',
+ scriptFileUploadCancelled: 'SQL file [{filename}] execution cancelled',
+ scriptFileUploadFailed: 'SQL file [{filename}] execution failed: {error}',
},
es: {
keywordPlaceholder: 'host / name / code',
diff --git a/frontend/src/i18n/en/machine.ts b/frontend/src/i18n/en/machine.ts
index 06ce2727..b8d13f1b 100644
--- a/frontend/src/i18n/en/machine.ts
+++ b/frontend/src/i18n/en/machine.ts
@@ -141,10 +141,20 @@ export default {
fileExceedsSysConf: 'The uploaded file exceeds the system configuration [{uploadMaxFileSize}]',
fileUploadSuccess: 'Machine file upload successful',
fileUploadFail: 'Machine file upload failed',
+ fileUpload: 'File Upload',
// Folder upload progress
folderUploadProgress: 'Folder Upload Progress',
+ folderUpload: 'Folder Upload',
uploading: 'Uploading',
concurrentFiles: '{count} concurrent',
+
+ // Upload notifications
+ uploadNotifications: {
+ title: 'Upload Notifications',
+ closeAll: 'Close All',
+ folders: '{count} folders',
+ files: '{count} files',
+ },
},
};
diff --git a/frontend/src/i18n/zh-cn/common.ts b/frontend/src/i18n/zh-cn/common.ts
index f65a337b..94de7a79 100644
--- a/frontend/src/i18n/zh-cn/common.ts
+++ b/frontend/src/i18n/zh-cn/common.ts
@@ -54,6 +54,8 @@ export default {
previousStep: '上一步',
nextStep: '下一步',
copy: '复制',
+ paste: '粘贴',
+ pasteFailed: '粘贴失败',
copySuccess: '复制成功',
copyCell: '复制单元格',
search: '搜索',
@@ -362,5 +364,13 @@ export default {
title: '请选择图标',
placeholder: '请输入内容搜索图标或者选择图标',
},
+
+ // 系统消息通知
+ sysmsg: {
+ notifications: {
+ title: '系统通知',
+ closeAll: '全部关闭',
+ },
+ },
},
};
diff --git a/frontend/src/i18n/zh-cn/db.ts b/frontend/src/i18n/zh-cn/db.ts
index aaf344fb..1fcf5e4f 100644
--- a/frontend/src/i18n/zh-cn/db.ts
+++ b/frontend/src/i18n/zh-cn/db.ts
@@ -219,5 +219,13 @@ export default {
running: '运行中',
waitRun: '待运行',
+
+ // SQL执行
+ sqlExecute: 'SQL执行',
+ executedStatements: '已执行',
+ elapsedTime: '已用时',
+ scriptFileUploadSuccess: 'SQL文件【{filename}】执行成功',
+ scriptFileUploadCancelled: 'SQL文件【{filename}】执行已取消',
+ scriptFileUploadFailed: 'SQL文件【{filename}】执行失败: {error}',
},
};
diff --git a/frontend/src/i18n/zh-cn/machine.ts b/frontend/src/i18n/zh-cn/machine.ts
index f08d5b38..e79f7b51 100644
--- a/frontend/src/i18n/zh-cn/machine.ts
+++ b/frontend/src/i18n/zh-cn/machine.ts
@@ -142,10 +142,20 @@ export default {
fileExceedsSysConf: '上传的文件超过系统配置的【{uploadMaxFileSize}】',
fileUploadSuccess: '机器文件上传成功',
fileUploadFail: '机器文件上传失败',
+ fileUpload: '文件上传',
// 文件夹上传进度
folderUploadProgress: '文件夹上传进度',
+ folderUpload: '文件夹上传',
uploading: '正在上传',
concurrentFiles: '{count} 个并发',
+
+ // 上传通知
+ uploadNotifications: {
+ title: '上传通知',
+ closeAll: '全部关闭',
+ folders: '{count} 个文件夹',
+ files: '{count} 个文件',
+ },
},
};
diff --git a/frontend/src/views/ops/db/api.ts b/frontend/src/views/ops/db/api.ts
index 6cb95524..9a9cd914 100644
--- a/frontend/src/views/ops/db/api.ts
+++ b/frontend/src/views/ops/db/api.ts
@@ -1,5 +1,7 @@
import Api from '@/common/Api';
import { AesEncrypt } from '@/common/crypto';
+import { joinClientParams } from '@/common/request';
+import { registerSqlExecAborter } from '@/components/sysmsg/db/db-sql-exec-progress';
export const dbApi = {
// 获取权限列表
@@ -79,3 +81,48 @@ export const encryptField = async (param: any, field: string) => {
}
return param;
};
+
+/**
+ * 上传SQL文件并执行
+ * @param file 文件对象
+ * @param params 上传参数
+ * @param options 上传选项
+ * @returns { uploadId: string; abort: () => void } 返回包含 uploadId 和中止方法的对象
+ */
+export function uploadSqlFile(
+ file: File,
+ params: {
+ dbId: number;
+ dbName: string;
+ },
+ options: {
+ onSuccess?: () => void;
+ onError?: (error: Error) => void;
+ } = {}
+): { uploadId: string; abort: () => void } {
+ // 生成 uploadId
+ const uploadId = `sql_exec_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
+
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('db', params.dbName);
+ formData.append('uploadId', uploadId);
+
+ // 创建 Api 实例
+ const api = Api.newPost(`/dbs/${params.dbId}/exec-sql-file`);
+
+ // 使用 Api.upload 发起请求
+ const { abort } = api.upload(formData, {
+ onSuccess: () => {
+ options.onSuccess?.();
+ },
+ onError: (error) => {
+ options.onError?.(error);
+ },
+ });
+
+ // 注册取消器(在获取到abort方法后)
+ registerSqlExecAborter(uploadId, abort);
+
+ return { uploadId, abort };
+}
diff --git a/frontend/src/views/ops/db/component/sqleditor/DbSqlEditor.vue b/frontend/src/views/ops/db/component/sqleditor/DbSqlEditor.vue
index 0e0bc492..664dd83a 100644
--- a/frontend/src/views/ops/db/component/sqleditor/DbSqlEditor.vue
+++ b/frontend/src/views/ops/db/component/sqleditor/DbSqlEditor.vue
@@ -20,6 +20,7 @@
class="sql-file-exec"
:before-upload="beforeUpload"
:on-success="execSqlFileSuccess"
+ :http-request="handleSqlFileUpload"
:headers="{ Authorization: token }"
:action="getUploadSqlFileUrl()"
:show-file-list="false"
@@ -141,7 +142,7 @@ import { editor } from 'monaco-editor';
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
import { DbInst } from '../../db';
-import { dbApi } from '../../api';
+import { dbApi, uploadSqlFile } from '../../api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { joinClientParams } from '@/common/request';
@@ -771,6 +772,29 @@ const beforeUpload = (file: File) => {
ElMessage.success(t('db.scriptFileUploadRunning', { filename: file.name }));
};
+// 自定义SQL文件上传处理
+const handleSqlFileUpload = (options: any) => {
+ const { file } = options;
+
+ const { uploadId, abort } = uploadSqlFile(
+ file,
+ {
+ dbId: props.dbId as number,
+ dbName: props.dbName as string,
+ },
+ {
+ onSuccess: () => {
+ ElMessage.success(t('db.scriptFileUploadSuccess', { filename: file.name }));
+ },
+ onError: (error) => {
+ ElMessage.error(t('db.scriptFileUploadFailed', { filename: file.name, error: error.message }));
+ },
+ }
+ );
+
+ return { abort };
+};
+
// 执行sql成功
const execSqlFileSuccess = (res: any) => {
if (res.code !== 200) {
diff --git a/frontend/src/views/ops/machine/api.ts b/frontend/src/views/ops/machine/api.ts
index d2535c61..c5f96396 100644
--- a/frontend/src/views/ops/machine/api.ts
+++ b/frontend/src/views/ops/machine/api.ts
@@ -1,11 +1,4 @@
-import Api from '@/common/Api';
-import { joinClientParams } from '@/common/request';
-import config from '@/common/config';
-import { randomUuid } from '@/common/utils/string';
-import { getToken } from '@/common/utils/storage';
-import { i18n } from '@/i18n';
-
-const t = i18n.global.t;
+import Api, { UploadOptions } from '@/common/Api';
export const machineApi = {
// 获取权限列表
@@ -41,7 +34,8 @@ export const machineApi = {
cpFile: Api.newPost('/machines/{machineId}/files/{fileId}/cp'),
renameFile: Api.newPost('/machines/{machineId}/files/{fileId}/rename'),
mvFile: Api.newPost('/machines/{machineId}/files/{fileId}/mv'),
- uploadFile: Api.newPost('/machines/{machineId}/files/{fileId}/upload?' + joinClientParams()),
+ uploadFile: Api.newUpload('/machines/{machineId}/files/{fileId}/upload'),
+ uploadFolder: Api.newPost('/machines/{machineId}/files/{fileId}/upload-folder'),
fileContent: Api.newGet('/machines/{machineId}/files/{fileId}/read'),
downloadFile: Api.newGet('/machines/{machineId}/files/{fileId}/download'),
createFile: Api.newPost('/machines/{machineId}/files/{id}/create-file'),
@@ -103,20 +97,6 @@ export interface UploadParams {
path: string;
/** 文件名 */
filename: string;
- /** 相对路径(文件夹上传时使用) */
- relativePath?: string;
-}
-
-/**
- * 文件上传选项
- */
-export interface UploadOptions {
- /** 进度回调 */
- onProgress?: (percent: number, uploadedSize: number, totalSize: number, speed: string) => void;
- /** 成功回调 */
- onSuccess?: () => void;
- /** 错误回调 */
- onError?: (error: Error) => void;
}
/**
@@ -124,13 +104,11 @@ export interface UploadOptions {
* @param file 文件对象
* @param params 上传参数
* @param options 上传选项
- * @returns Promise
+ * @returns { uploadId: string; abort: () => void } 返回包含 uploadId 和中止方法的对象
*/
-export async function uploadFile(file: File, params: UploadParams, options: UploadOptions = {}): Promise {
- const { onProgress, onSuccess, onError } = options;
-
- // 如果没有 uploadId,自动生成
- const uploadId = params.uploadId || randomUuid();
+export function uploadFile(file: File, params: UploadParams, options: UploadOptions = {}): { uploadId: string; abort: () => void } {
+ // 业务层生成 uploadId
+ const uploadId = params.uploadId || `upload_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
const formData = new FormData();
formData.append('file', file);
@@ -141,54 +119,9 @@ export async function uploadFile(file: File, params: UploadParams, options: Uplo
formData.append('fileId', String(params.fileId));
formData.append('path', params.path);
- if (params.relativePath) {
- formData.append('relativePath', params.relativePath);
- }
+ const { abort } = machineApi.uploadFile.upload(formData, options);
- const token = getToken();
- const url = `${config.baseApiUrl}/machines/${params.machineId}/files/${params.fileId}/upload?token=${token}`;
-
- try {
- const xhr = new XMLHttpRequest();
-
- // 进度回调
- xhr.upload.onprogress = (event) => {
- if (event.lengthComputable && onProgress) {
- const percent = Math.round((event.loaded / event.total) * 100);
- const elapsed = (Date.now() - startTime) / 1000;
- const speedBytes = elapsed > 0 ? event.loaded / elapsed : 0;
- let speed = '0 B/s';
- if (speedBytes < 1024) {
- speed = `${speedBytes.toFixed(0)} B/s`;
- } else if (speedBytes < 1024 * 1024) {
- speed = `${(speedBytes / 1024).toFixed(1)} KB/s`;
- } else {
- speed = `${(speedBytes / (1024 * 1024)).toFixed(1)} MB/s`;
- }
- onProgress(percent, event.loaded, event.total, speed);
- }
- };
-
- // 完成回调
- xhr.onload = () => {
- if (xhr.status === 200) {
- onSuccess?.();
- } else {
- onError?.(new Error(t('common.uploadFailed', { error: `HTTP ${xhr.status}` })));
- }
- };
-
- // 错误回调
- xhr.onerror = () => {
- onError?.(new Error(t('common.uploadFailed', { error: '网络错误' })));
- };
-
- const startTime = Date.now();
- xhr.open('POST', url);
- xhr.send(formData);
- } catch (error: any) {
- onError?.(new Error(t('common.uploadFailed', { error: error.message })));
- }
+ return { uploadId, abort };
}
/**
@@ -210,27 +143,15 @@ export interface FolderUploadParams {
}
/**
- * 文件夹上传选项
- */
-export interface FolderUploadOptions {
- /** 成功回调 */
- onSuccess?: () => void;
- /** 错误回调 */
- onError?: (error: Error) => void;
-}
-
-/**
- * 上传文件夹(使用 /upload-folder 接口)
+ * 上传文件夹(使用 /upload-folder 接口)
* @param files 文件列表
* @param params 上传参数
* @param options 上传选项
- * @returns Promise
+ * @returns { uploadId: string; abort: () => void } 返回包含 uploadId 和中止方法的对象
*/
-export async function uploadFolder(files: FileList | File[], params: FolderUploadParams, options: FolderUploadOptions = {}): Promise {
- const { onSuccess, onError } = options;
-
- // 如果没有 uploadId,自动生成
- const uploadId = params.uploadId || randomUuid();
+export function uploadFolder(files: FileList | File[], params: FolderUploadParams, options: UploadOptions = {}): { uploadId: string; abort: () => void } {
+ // 业务层生成 uploadId
+ const uploadId = params.uploadId || `upload_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
const formData = new FormData();
formData.append('uploadId', uploadId);
@@ -253,29 +174,8 @@ export async function uploadFolder(files: FileList | File[], params: FolderUploa
formData.append('paths', path);
});
- const token = getToken();
- const url = `${config.baseApiUrl}/machines/${params.machineId}/files/${params.fileId}/upload-folder?token=${token}`;
+ // 使用 Api.upload 发起请求
+ const { abort } = machineApi.uploadFolder.upload(formData, options);
- try {
- const xhr = new XMLHttpRequest();
-
- // 完成回调
- xhr.onload = () => {
- if (xhr.status === 200) {
- onSuccess?.();
- } else {
- onError?.(new Error(t('common.uploadFailed', { error: `HTTP ${xhr.status}` })));
- }
- };
-
- // 错误回调
- xhr.onerror = () => {
- onError?.(new Error(t('common.uploadFailed', { error: '网络错误' })));
- };
-
- xhr.open('POST', url);
- xhr.send(formData);
- } catch (error: any) {
- onError?.(new Error(t('common.uploadFailed', { error: error.message })));
- }
+ return { uploadId, abort };
}
diff --git a/frontend/src/views/ops/machine/file/MachineFile.vue b/frontend/src/views/ops/machine/file/MachineFile.vue
index 3dd39e66..a9a0d66b 100755
--- a/frontend/src/views/ops/machine/file/MachineFile.vue
+++ b/frontend/src/views/ops/machine/file/MachineFile.vue
@@ -309,6 +309,8 @@
import { ElInput, ElMessage } from 'element-plus';
import { computed, defineAsyncComponent, onMounted, reactive, ref, toRefs } from 'vue';
import { machineApi, uploadFile, uploadFolder } from '../api';
+import { registerUploadAborter } from '@/components/sysmsg/machine/machine-file-upload-progress';
+import { registerFolderUploadAborter } from '@/components/sysmsg/machine/machine-folder-upload-progress';
import { isTrue, notBlank } from '@/common/assert';
import config from '@/common/config';
@@ -789,10 +791,8 @@ function handleFolderUpload(e: any) {
return;
}
- console.log('[MachineFile] Folder upload:', files.length, 'files, total size:', totalFileSize);
-
// 使用文件夹上传接口
- uploadFolder(
+ const { uploadId, abort } = uploadFolder(
files,
{
machineId: props.machineId as number,
@@ -814,6 +814,9 @@ function handleFolderUpload(e: any) {
}
);
+ // 注册取消方法
+ registerFolderUploadAborter(uploadId, abort);
+
// 清空已选择的文件夹
const folderEle: any = document.getElementById('folderUploadInput');
if (folderEle) {
@@ -831,7 +834,7 @@ const handleFileUpload = (content: any) => {
}
// 上传文件
- uploadFile(
+ const { uploadId, abort } = uploadFile(
file,
{
machineId: props.machineId as number,
@@ -853,6 +856,9 @@ const handleFileUpload = (content: any) => {
},
}
);
+
+ // 注册取消方法
+ registerUploadAborter(uploadId, abort);
};
const uploadSuccess = (res: any) => {
diff --git a/server/internal/db/api/db.go b/server/internal/db/api/db.go
index a2e3a6ff..dc33df7a 100644
--- a/server/internal/db/api/db.go
+++ b/server/internal/db/api/db.go
@@ -166,15 +166,17 @@ func (d *Db) ExecSql(rc *req.Ctx) {
// 执行sql文件
func (d *Db) ExecSqlFile(rc *req.Ctx) {
- multipart, err := rc.GetRequest().MultipartReader()
- biz.ErrIsNilAppendErr(err, "failed to read sql file: %s")
- file, err := multipart.NextPart()
+ fileheader, err := rc.FormFile("file")
+ biz.ErrIsNilAppendErr(err, "read form file error: %s")
+
+ file, err := fileheader.Open()
biz.ErrIsNilAppendErr(err, "failed to read sql file: %s")
defer file.Close()
- filename := file.FileName()
+ filename := fileheader.Filename
dbId := getDbId(rc)
- dbName := getDbName(rc)
clientId := rc.Query("clientId")
+ dbName := rc.PostForm("db")
+ uploadId := rc.PostForm("uploadId")
dbConn, err := d.dbApp.GetDbConn(rc.MetaCtx, dbId, dbName)
biz.ErrIsNil(err)
@@ -186,6 +188,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
Filename: filename,
DbConn: dbConn,
ClientId: clientId,
+ UploadId: uploadId,
}))
}
diff --git a/server/internal/db/application/db.go b/server/internal/db/application/db.go
index 8061c8a3..3daa6996 100644
--- a/server/internal/db/application/db.go
+++ b/server/internal/db/application/db.go
@@ -187,6 +187,7 @@ func (d *dbAppImpl) GetDbConn(ctx context.Context, dbId uint64, dbName string) (
}
di.CodePath = d.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeDb), db.Code)
di.Id = db.Id
+ di.DbCode = db.Code
checkDb := di.GetDatabase()
if db.GetDatabaseMode == entity.DbGetDatabaseModeAssign && !strings.Contains(" "+db.Database+" ", " "+checkDb+" ") {
diff --git a/server/internal/db/application/db_sql_exec.go b/server/internal/db/application/db_sql_exec.go
index c39360fa..e3ef0741 100644
--- a/server/internal/db/application/db_sql_exec.go
+++ b/server/internal/db/application/db_sql_exec.go
@@ -184,21 +184,23 @@ func (d *dbSqlExecAppImpl) ExecReader(ctx context.Context, execReader *dto.SqlRe
clientId := execReader.ClientId
filename := stringx.Truncate(execReader.Filename, 20, 10, "...")
la := contextx.GetLoginAccount(ctx)
- needSendMsg := la != nil && clientId != ""
+ needSendMsg := la != nil && clientId != "" && execReader.UploadId != ""
startTime := time.Now()
executedStatements := 0
- progressId := stringx.Rand(32)
+ dbInfo := dbConn.Info
msgEvent := &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplSqlScriptRunSuccess,
- Params: collx.M{"filename": filename, "dbId": dbConn.Info.Id, "dbName": dbConn.Info.Name},
+ Params: collx.M{"filename": filename, "dbId": dbInfo.Id, "dbName": dbInfo.Name},
}
progressMsgEvent := &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplSqlScriptRunProgress,
Params: collx.M{
- "id": progressId,
+ "id": execReader.UploadId,
+ "dbCode": dbInfo.DbCode,
+ "dbName": dbInfo.Name,
"title": filename,
"executedStatements": executedStatements,
"terminated": false,
@@ -233,6 +235,16 @@ func (d *dbSqlExecAppImpl) ExecReader(ctx context.Context, execReader *dto.SqlRe
// 使用方言切割器进行 SQL 切割
splitter := dbConn.GetDialect().GetSQLSplitter()
err := splitter.SplitSQL(execReader.Reader, func(sql string) error {
+ // 检查context是否已取消
+ if ctx.Err() != nil {
+ if needSendMsg {
+ progressMsgEvent.Params["executedStatements"] = executedStatements
+ progressMsgEvent.Params["status"] = "cancelled"
+ global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, progressMsgEvent)
+ }
+ return errorx.NewBizI(ctx, imsg.ErrSqlExecCancelled)
+ }
+
if executedStatements%50 == 0 {
if needSendMsg {
progressMsgEvent.Params["executedStatements"] = executedStatements
diff --git a/server/internal/db/application/dto/sql_exec.go b/server/internal/db/application/dto/sql_exec.go
index de8ffb9e..c1818c4b 100644
--- a/server/internal/db/application/dto/sql_exec.go
+++ b/server/internal/db/application/dto/sql_exec.go
@@ -28,4 +28,5 @@ type SqlReaderExec struct {
Filename string
ClientId string // 客户端id,若存在则会向其发送执行进度消息
+ UploadId string // 上传id,用于记录上传进度
}
diff --git a/server/internal/db/dbm/dbi/db_info.go b/server/internal/db/dbm/dbi/db_info.go
index 3377a56c..6eb81be6 100644
--- a/server/internal/db/dbm/dbi/db_info.go
+++ b/server/internal/db/dbm/dbi/db_info.go
@@ -27,6 +27,7 @@ type DbInfo struct {
InstanceId uint64 // 实例id
Id uint64 // dbId
+ DbCode string
Name string
Type DbType // 类型,mysql postgres等
diff --git a/server/internal/db/imsg/en.go b/server/internal/db/imsg/en.go
index a67401e7..00bf72c7 100644
--- a/server/internal/db/imsg/en.go
+++ b/server/internal/db/imsg/en.go
@@ -20,6 +20,7 @@ var En = map[i18n.MsgId]string{
ErrExistRunFailSql: "There is an execution error in sql",
ErrNeedSubmitWorkTicket: "This operation needs to submit a work ticket for approval",
+ ErrSqlExecCancelled: "SQL execution cancelled",
// db transfer
LogDtsSave: "dts - Save data transfer task",
diff --git a/server/internal/db/imsg/imsg.go b/server/internal/db/imsg/imsg.go
index 7fe73bd0..bfb7544b 100644
--- a/server/internal/db/imsg/imsg.go
+++ b/server/internal/db/imsg/imsg.go
@@ -30,6 +30,7 @@ const (
ErrExistRunFailSql
ErrNeedSubmitWorkTicket
+ ErrSqlExecCancelled
// db transfer
LogDtsSave
diff --git a/server/internal/db/imsg/zh_cn.go b/server/internal/db/imsg/zh_cn.go
index 715596eb..742542e1 100644
--- a/server/internal/db/imsg/zh_cn.go
+++ b/server/internal/db/imsg/zh_cn.go
@@ -20,6 +20,7 @@ var Zh_CN = map[i18n.MsgId]string{
ErrExistRunFailSql: "存在执行错误的sql",
ErrNeedSubmitWorkTicket: "该操作需要提交工单审批执行",
+ ErrSqlExecCancelled: "SQL执行已取消",
// db transfer
LogDtsSave: "dts-保存数据迁移任务",
diff --git a/server/internal/machine/api/machine_file.go b/server/internal/machine/api/machine_file.go
index b2bbedcc..782b7442 100644
--- a/server/internal/machine/api/machine_file.go
+++ b/server/internal/machine/api/machine_file.go
@@ -100,6 +100,13 @@ type progressReader struct {
}
func (r *progressReader) Read(p []byte) (n int, err error) {
+ // 先检查 context 是否已取消
+ select {
+ case <-r.ctx.Done():
+ return 0, r.ctx.Err()
+ default:
+ }
+
n, err = r.reader.Read(p)
if n > 0 {
r.readSize += int64(n)
@@ -339,6 +346,27 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
mi, err = m.machineFileApp.UploadFile(ctx, opForm, fileheader.Filename, reader)
rc.ReqParam = collx.Kvs("machine", mi, "path", fmt.Sprintf("%s/%s", path, fileheader.Filename))
+ // 检查是否是取消操作
+ if err != nil {
+ logx.ErrorfContext(ctx, "[UploadFile] Upload error: %v, uploadId: %s, ctx.Err: %v", err, uploadId, ctx.Err())
+ if ctx.Err() != nil {
+ logx.InfofContext(ctx, "File upload cancelled by client: %s, uploadId: %s", fileheader.Filename, uploadId)
+ // 发送取消通知
+ if hasProgressNotify {
+ progressMsgEvent := &msgdto.MsgTmplSendEvent{
+ TmplChannel: msgdto.MsgTmplMachineFileUploadProgress,
+ Params: collx.M{
+ "uploadId": uploadId,
+ "status": "error",
+ },
+ ReceiverIds: []uint64{contextx.GetLoginAccount(ctx).Id},
+ }
+ global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, progressMsgEvent)
+ }
+ return
+ }
+ }
+
// 发送完成通知
if hasProgressNotify && err == nil {
progressMsgEvent := &msgdto.MsgTmplSendEvent{
@@ -477,7 +505,7 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
for _, chunk := range chunks {
wg.Go(func() {
defer gox.Recover(func(e error) {
- logx.Errorf("upload folder error: %s", e)
+ logx.ErrorfContext(ctx, "upload folder error: %s", e)
})
for _, file := range chunk {
@@ -495,7 +523,7 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
createfile, err := sftpCli.Create(fmt.Sprintf("%s/%s/%s", basePath, dir, fileHeader.Filename))
if err != nil {
- logx.Errorf("create file error: %s", err)
+ logx.ErrorfContext(ctx, "create file error: %s", err)
file.Close()
// 从正在上传列表移除
@@ -539,7 +567,6 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
"path": basePath,
"uploadId": uploadId,
"folderName": folderName,
- "lastFile": fullPath,
"totalFiles": totalFiles,
"uploadedFiles": uploadedFiles,
"totalSize": totalSize,
@@ -559,7 +586,14 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
_, err = io.Copy(createfile, reader)
if err != nil {
- logx.Errorf("copy file error: %s", err)
+ logx.ErrorfContext(ctx, "copy file error: %s, uploadId: %s", err, uploadId)
+ // 检查是否是取消操作
+ if ctx.Err() != nil {
+ logx.InfofContext(ctx, "Folder upload cancelled by client, uploadId: %s", uploadId)
+ file.Close()
+ createfile.Close()
+ return
+ }
}
// 累加已上传大小
diff --git a/server/internal/machine/application/machine_file.go b/server/internal/machine/application/machine_file.go
index 33eeefe0..51e29d53 100644
--- a/server/internal/machine/application/machine_file.go
+++ b/server/internal/machine/application/machine_file.go
@@ -88,7 +88,6 @@ type machineFileAppImpl struct {
var _ MachineFile = (*machineFileAppImpl)(nil)
-
// 分页获取机器文件配置信息列表
func (m *machineFileAppImpl) GetPageList(condition *entity.MachineFile, pageParam model.PageParam, orderBy ...string) (*model.PageResult[*entity.MachineFile], error) {
return m.GetRepo().GetPageList(condition, pageParam, orderBy...)
@@ -305,7 +304,17 @@ func (m *machineFileAppImpl) UploadFile(ctx context.Context, opParam *dto.Machin
return nil, err
}
defer file.Close()
- io.Copy(file, reader)
+
+ // 使用 io.Copy 并检查错误
+ _, err = io.Copy(file, reader)
+ if err != nil {
+ // 检查是否是连接断开导致的错误
+ if ctx.Err() != nil {
+ logx.WarnfContext(ctx, "Upload file cancelled by client: %s", filename)
+ return nil, ctx.Err()
+ }
+ return nil, fmt.Errorf("copy file error: %w", err)
+ }
return &mcm.MachineInfo{Name: opParam.AuthCertName, Ip: opParam.AuthCertName}, nil
}
@@ -319,7 +328,17 @@ func (m *machineFileAppImpl) UploadFile(ctx context.Context, opParam *dto.Machin
return mi, err
}
defer createfile.Close()
- io.Copy(createfile, reader)
+
+ // 使用 io.Copy 并检查错误
+ _, err = io.Copy(createfile, reader)
+ if err != nil {
+ // 检查是否是连接断开导致的错误
+ if ctx.Err() != nil {
+ logx.WarnfContext(ctx, "Upload file cancelled by client: %s", filename)
+ return mi, ctx.Err()
+ }
+ return mi, fmt.Errorf("copy file error: %w", err)
+ }
return mi, err
}
diff --git a/server/internal/pkg/config/app.go b/server/internal/pkg/config/app.go
index 43b33474..9fe95f49 100644
--- a/server/internal/pkg/config/app.go
+++ b/server/internal/pkg/config/app.go
@@ -1,5 +1,5 @@
package config
const (
- Version = "v1.11.1"
+ Version = "v1.11.2"
)