From 13288941884a06a507a89722828bfa36ab606546 Mon Sep 17 00:00:00 2001 From: lijiahangmax Date: Sat, 28 Jun 2025 01:25:54 +0800 Subject: [PATCH] =?UTF-8?q?:hammer:=20=E6=B7=BB=E5=8A=A0=20RDP=20=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8A=9F=E8=83=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/store/modules/terminal/index.ts | 4 +- .../src/store/modules/terminal/types.ts | 4 +- .../components/view/rdp/rdp-action-bar.vue | 7 +- .../src/views/terminal/interfaces/transfer.ts | 137 +++++++++----- .../service/transfer/base-transfer-manager.ts | 67 +++++++ .../service/transfer/rdp-file-upload-task.ts | 174 ++++++++++++++++++ .../service/transfer/rdp-transfer-manager.ts | 68 +++++++ 7 files changed, 407 insertions(+), 54 deletions(-) create mode 100644 orion-visor-ui/src/views/terminal/service/transfer/base-transfer-manager.ts create mode 100644 orion-visor-ui/src/views/terminal/service/transfer/rdp-file-upload-task.ts create mode 100644 orion-visor-ui/src/views/terminal/service/transfer/rdp-transfer-manager.ts diff --git a/orion-visor-ui/src/store/modules/terminal/index.ts b/orion-visor-ui/src/store/modules/terminal/index.ts index 3e32a5fb..a934a692 100644 --- a/orion-visor-ui/src/store/modules/terminal/index.ts +++ b/orion-visor-ui/src/store/modules/terminal/index.ts @@ -34,7 +34,7 @@ import { TerminalSessionTypes, TerminalTabs } from '@/views/terminal/types/const import TerminalTabManager from '@/views/terminal/service/tab/terminal-tab-manager'; import TerminalPanelManager from '@/views/terminal/service/tab/terminal-panel-manager'; import TerminalSessionManager from '@/views/terminal/service/session/terminal-session-manager'; -import SftpTransferManager from '@/views/terminal/service/transfer/sftp-transfer-manager'; +import TerminalTransferManager from '@/views/terminal/service/transfer/terminal-transfer-manager'; // 终端偏好项 export const TerminalPreferenceItem = { @@ -90,7 +90,7 @@ export default defineStore('terminal', { tabManager: new TerminalTabManager(), panelManager: new TerminalPanelManager(), sessionManager: markRaw(new TerminalSessionManager()), - transferManager: new SftpTransferManager(), + transferManager: new TerminalTransferManager(), }), actions: { diff --git a/orion-visor-ui/src/store/modules/terminal/types.ts b/orion-visor-ui/src/store/modules/terminal/types.ts index 7a6e84a6..7c1a5119 100644 --- a/orion-visor-ui/src/store/modules/terminal/types.ts +++ b/orion-visor-ui/src/store/modules/terminal/types.ts @@ -1,4 +1,4 @@ -import type { ISftpTransferManager, ITerminalPanelManager, ITerminalSessionManager, ITerminalTabManager, TerminalTheme } from '@/views/terminal/interfaces'; +import type { ITerminalPanelManager, ITerminalSessionManager, ITerminalTabManager, ITerminalTransferManager, TerminalTheme } from '@/views/terminal/interfaces'; import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data'; export interface TerminalState { @@ -8,7 +8,7 @@ export interface TerminalState { tabManager: ITerminalTabManager; panelManager: ITerminalPanelManager; sessionManager: ITerminalSessionManager; - transferManager: ISftpTransferManager; + transferManager: ITerminalTransferManager; } // 终端配置 diff --git a/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue b/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue index d3dd25bc..3dd6642b 100644 --- a/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue +++ b/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue @@ -138,7 +138,6 @@ } from '@/views/terminal/types/const'; import { computed, ref, watch, onMounted } from 'vue'; import { setAutoFocus } from '@/utils/dom'; - import { Message } from '@arco-design/web-vue'; import { saveAs } from 'file-saver'; import { readText } from '@/hooks/copy'; import { useTerminalStore, useDictStore } from '@/store'; @@ -150,7 +149,7 @@ direction: string; }>(); - const { preference } = useTerminalStore(); + const { preference, transferManager } = useTerminalStore(); const { toOptions, getDictValue } = useDictStore(); const { visible, setVisible } = useVisible(); @@ -276,8 +275,8 @@ // 上传文件 const uploadFile = () => { - // TODO 上传功能 - Message.warning('暂不支持文件上传, 等下版本携带'); + transferManager.rdp.addUpload(props.session, fileList.value[0].file as File); + fileList.value = []; }; // 选择文件回调 diff --git a/orion-visor-ui/src/views/terminal/interfaces/transfer.ts b/orion-visor-ui/src/views/terminal/interfaces/transfer.ts index 4055813d..8f538b71 100644 --- a/orion-visor-ui/src/views/terminal/interfaces/transfer.ts +++ b/orion-visor-ui/src/views/terminal/interfaces/transfer.ts @@ -1,42 +1,84 @@ -import type { SftpFile } from '@/views/terminal/interfaces'; +import type { IRdpSession, SftpFile } from '@/views/terminal/interfaces'; +import type { Reactive } from 'vue'; +import type Guacamole from 'guacamole-common-js'; + +// 终端文件传输管理器定义 +export interface ITerminalTransferManager { + sftp: ISftpTransferManager; + rdp: IRdpTransferManager; +} + +// 文件传输文件项 +export interface FileTransferItem { + name: string; + parentPath: string; + size: number; + file?: File; + paths?: Array; + unknownSize?: boolean; +} + +// 文件传输文件项 +export interface FileTransferReactiveState { + currentSize: number, + totalSize: number; + progress: number | string; + status: string; + errorMessage?: string; + finished: boolean; + aborted: boolean; +} + +// 文件传输管理器定义 +export interface ITransferManager { + // 传输的文件列表 + tasks: Array>; -// sftp 传输管理器定义 -export interface ISftpTransferManager { - transferList: Array; - // 添加上传任务 - addUpload: (hostId: number, parentPath: string, files: Array) => void; - // 添加下载任务 - addDownload: (hostId: number, currentPath: string, files: Array) => void; // 取消传输 cancelTransfer: (fileId: string) => void; - // 取消全部传输 + // 取消全部执行中的传输 cancelAllTransfer: () => void; } -// sftp 传输处理回调定义 -export interface ISftpTransferCallback { - // 下一分片回调 - onNextPart: () => Promise; - // 开始回调 - onStart: (channelId: string, token: string) => void; - // 进度回调 - onProgress: (totalSize: number | undefined, currentSize: number | undefined) => void; - // 失败回调 - onError: (msg: string | undefined) => void; - // 完成回调 - onFinish: () => void; - // 中断回调 - onAbort: () => void; +// SFTP 文件传输管理器定义 +export interface ISftpTransferManager extends ITransferManager { + // 添加上传任务 + addUpload: (hostId: number, parentPath: string, files: Array) => Promise; + // 添加下载任务 + addDownload: (hostId: number, currentPath: string, files: Array) => Promise; } -// sftp 传输处理器定义 -export interface ISftpTransferHandler extends ISftpTransferCallback { - // 类型 +// RDP 文件传输管理器定义 +export interface IRdpTransferManager extends ITransferManager { + // 添加上传任务 + addUpload: (session: IRdpSession, file: File) => Promise; + // 添加下载任务 + addDownload: (session: IRdpSession, stream: Guacamole.InputStream, mimetype: string, name: string) => void; + // 通过 sessionKey 关闭 + closeBySessionKey: (sessionKey: string) => void; +} + +// 文件传输任务类型 +export type FileTransferTaskType = IFileUploadTask | IFileDownloadTask; +export type MaybeFileTransferTask = IFileUploadTask & IFileDownloadTask; + +// 设置传输客户端 +export interface ISetTransferClient { + setClient: (client: T) => void; +} + +// 文件传输任务定义 +export interface IFileTransferTask { type: string; - // 是否完成 - finished: boolean; - // 是否中断 - aborted: boolean; + source: string; + fileId: string; + hostId: number; + sessionKey: string; + // 文件项 + fileItem: FileTransferItem; + // 状态 + state: Reactive; + // 开始 start: () => void; // 完成 @@ -45,24 +87,27 @@ export interface ISftpTransferHandler extends ISftpTransferCallback { error: () => void; // 中断 abort: () => void; - // 是否有下一个分片 - hasNextPart: () => boolean; + + // 传输完成回调 + onFinish: () => void; + // 传输失败回调 + onError: (msg: string | undefined) => void; + // 传输中断回调 + onAbort: () => void; } -// sftp 上传文件项 -export interface SftpTransferItem { - fileId: string; - type: string; - hostId: number; - name: string; - parentPath: string; - currentSize: number, - totalSize: number; - progress: number | string; - status: string; - errorMessage?: string; - file: File; - paths?: Array; +// 文件上传任务定义 +export interface IFileUploadTask extends IFileTransferTask { + // 请求上传下一个分片 + onNextPart: () => Promise; +} + +// 文件下载任务定义 +export interface IFileDownloadTask extends IFileTransferTask { + // 开始下载回调 + onStart: (channelId: string, token: string) => void; + // 下载进度回调 + onProgress: (totalSize: number | undefined, currentSize: number | undefined) => void; } // 传输操作响应 diff --git a/orion-visor-ui/src/views/terminal/service/transfer/base-transfer-manager.ts b/orion-visor-ui/src/views/terminal/service/transfer/base-transfer-manager.ts new file mode 100644 index 00000000..991a3059 --- /dev/null +++ b/orion-visor-ui/src/views/terminal/service/transfer/base-transfer-manager.ts @@ -0,0 +1,67 @@ +import type { ITransferManager, FileTransferTaskType } from '@/views/terminal/interfaces'; +import { TransferStatus, TerminalMessages } from '@/views/terminal/types/const'; + +// 传输管理器基类 +export default abstract class BaseTransferManager implements ITransferManager { + + public tasks: Array; + + protected progressIntervalId?: number; + + protected constructor() { + this.tasks = []; + } + + // 取消传输 + abstract cancelTransfer(fileId: string): void; + + // 取消全部执行中的传输 + cancelAllTransfer(): void { + // 从列表中移除非传输中的元素 + this.tasks.reduceRight((_, value: FileTransferTaskType, index: number) => { + if (value.state.status !== TransferStatus.TRANSFERRING) { + this.tasks.splice(index, 1); + } + }, null as any); + } + + // 计算传输进度 + protected calculateProgress(): void { + let count = 0; + this.tasks.forEach(task => { + const state = task.state; + if (state.totalSize !== 0 + && (state.status === TransferStatus.WAITING || state.status === TransferStatus.TRANSFERRING) + && task.fileItem.unknownSize !== true) { + count++; + state.progress = (state.currentSize / state.totalSize * 100).toFixed(2); + } + }); + // 如果所有任务都已结束则关闭 + if (count === 0) { + clearInterval(this.progressIntervalId); + } + }; + + // 重置进度定时器 + protected resetProgressTimer(): void { + clearInterval(this.progressIntervalId); + this.progressIntervalId = window.setInterval(this.calculateProgress.bind(this), 500); + } + + // 关闭 + protected close(): void { + // 关闭传输进度 + clearInterval(this.progressIntervalId); + // 进行中和等待中的文件改为失败 + this.tasks.forEach(task => { + const state = task.state; + if (state.status === TransferStatus.WAITING || + state.status === TransferStatus.TRANSFERRING) { + state.status = TransferStatus.ERROR; + state.errorMessage = TerminalMessages.sessionClosed; + } + }); + } + +} diff --git a/orion-visor-ui/src/views/terminal/service/transfer/rdp-file-upload-task.ts b/orion-visor-ui/src/views/terminal/service/transfer/rdp-file-upload-task.ts new file mode 100644 index 00000000..c0ac9052 --- /dev/null +++ b/orion-visor-ui/src/views/terminal/service/transfer/rdp-file-upload-task.ts @@ -0,0 +1,174 @@ +import type { FileTransferItem, IFileUploadTask, IRdpSession } from '@/views/terminal/interfaces'; +import { TransferType, TransferSource, TerminalMessages, TransferStatus } from '@/views/terminal/types/const'; +import { closeFileReader } from '@/utils/file'; +import BaseFileTransferTask from './base-file-transfer-task'; +import Guacamole from 'guacamole-common-js'; + +// 6048 +export const PART_SIZE = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH; + +// rdp 上传任务实现 +export default class RdpFileUploadTask extends BaseFileTransferTask implements IFileUploadTask { + + private session: IRdpSession; + private writer?: Guacamole.ArrayBufferWriter; + private stream?: Guacamole.OutputStream; + private currentPart: number; + private readonly totalPart: number; + + constructor(session: IRdpSession, fileItem: FileTransferItem) { + super(TransferType.UPLOAD, TransferSource.RDP, session.info.hostId, session.sessionKey, fileItem, {}); + this.session = session; + this.currentPart = 0; + this.totalPart = Math.ceil(fileItem.size / PART_SIZE); + } + + // 开始上传 + start() { + this.state.status = TransferStatus.TRANSFERRING; + try { + const file = this.fileItem.file as File; + const client = this.session.client; + // 创建文件流 + this.stream = client.createFileStream(file.type, file.name); + this.writer = new Guacamole.ArrayBufferWriter(this.stream); + // 分片上传完成回调 + this.writer.onack = ({ code, message }) => { + // 成功继续上传分片 + if (code === Guacamole.Status.Code.SUCCESS) { + this.onNextPart(); + return; + } + // 失败关闭流 + if (this.stream) { + this.stream.sendEnd(); + } + // 响应错误 + this.onError(message); + }; + // 开始上传分片 + this.onNextPart(); + } catch (e) { + this.onError(TerminalMessages.fileTransferError); + } + } + + // 上传中断 + abort() { + this.onAbort(); + } + + // 上传完成 + finish() { + this.onFinish(); + } + + // 上传失败 + error(): void { + this.onError(undefined as unknown as string); + } + + // 上传下一个分片 + async onNextPart() { + // 完成或者中断直接跳过 + if (this.state.aborted || this.state.finished) { + return; + } + if (this.hasNextPart()) { + try { + // 有下一个分片则上传 + await this.uploadNextPart(); + } catch (e) { + // 读取文件失败 + this.error(); + } + } else { + this.finish(); + } + } + + // 执行上传下一分片 + private async uploadNextPart() { + // 读取数据 + const start = this.currentPart * PART_SIZE; + const end = Math.min(this.fileItem.size, start + PART_SIZE); + const chunk = (this.fileItem.file as File).slice(start, end); + const reader = new FileReader(); + try { + const arrayBuffer = await new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result); + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(chunk); + }); + // 发送数据 + this.writer?.sendData(arrayBuffer as ArrayBuffer); + this.currentPart++; + this.state.currentSize += (end - start); + } finally { + // 释放资源 + closeFileReader(reader); + } + } + + // 上传中断 + onAbort() { + if (this.state.aborted || this.state.finished) { + return; + } + this.state.aborted = true; + try { + if (this.session.status.connected) { + // 关闭流 + if (this.stream) { + this.stream.sendEnd(); + } + } + // 触发失败 + this.onError(TerminalMessages.sessionClosed); + } catch (e) { + // 触发失败 + this.onError(TerminalMessages.fileTransferError); + } + } + + // 上传完成 + onFinish() { + if (this.state.aborted || this.state.finished) { + return; + } + this.state.finished = true; + this.state.progress = 100; + this.state.status = TransferStatus.SUCCESS; + // 关闭流 + if (this.stream) { + this.stream.sendEnd(); + } + // 释放资源 + this.releaseResource(); + } + + // 上传错误回调 + onError(msg: string | undefined) { + this.state.finished = true; + this.state.status = TransferStatus.ERROR; + this.state.errorMessage = msg || TerminalMessages.fileTransferError; + // 释放资源 + this.releaseResource(); + } + + // 是否有下一个分片 + private hasNextPart() { + return this.currentPart < this.totalPart; + } + + // 释放资源 + private releaseResource() { + if (this.writer) { + this.writer.onack = null; + } + this.writer = undefined; + this.stream = undefined; + this.fileItem.file = undefined; + } + +} diff --git a/orion-visor-ui/src/views/terminal/service/transfer/rdp-transfer-manager.ts b/orion-visor-ui/src/views/terminal/service/transfer/rdp-transfer-manager.ts new file mode 100644 index 00000000..ed04e56f --- /dev/null +++ b/orion-visor-ui/src/views/terminal/service/transfer/rdp-transfer-manager.ts @@ -0,0 +1,68 @@ +import type { IRdpTransferManager, IRdpSession } from '@/views/terminal/interfaces'; +import type Guacamole from 'guacamole-common-js'; +import { TerminalMessages } from '../../types/const'; +import { Message } from '@arco-design/web-vue'; +import BaseTransferManager from './base-transfer-manager'; +import RdpFileDownloadTask from './rdp-file-download-task'; +import RdpFileUploadTask from './rdp-file-upload-task'; + +// RDP 传输管理器实现 +export default class RdpTransferManager extends BaseTransferManager implements IRdpTransferManager { + + constructor() { + super(); + } + + // 添加上传任务 + async addUpload(session: IRdpSession, file: File) { + Message.info(TerminalMessages.fileUploading); + // 创建任务 + const task = new RdpFileUploadTask(session, { + name: file.webkitRelativePath || file.name, + parentPath: session.fileSystemName, + size: file.size, + file, + }); + this.tasks.push(task); + // 开始上传 + task.start(); + // 开始计算进度 + this.resetProgressTimer(); + } + + // 添加下载任务 + addDownload(session: IRdpSession, stream: Guacamole.InputStream, mimetype: string, name: string) { + Message.info(TerminalMessages.fileDownloading); + // 创建任务 + const task = new RdpFileDownloadTask(session, stream, mimetype, { + name, + parentPath: session.fileSystemName, + size: 0, + unknownSize: true, + }); + this.tasks.push(task); + // 开始下载 + task.start(); + // 开始计算进度 + this.resetProgressTimer(); + } + + // 取消传输 + cancelTransfer(fileId: string): void { + const index = this.tasks.findIndex(s => s.fileId === fileId); + if (index === -1) { + return; + } + // 中断 + this.tasks[index].abort(); + // 从列表中移除 + this.tasks.splice(index, 1); + } + + // 通过 sessionKey 关闭 + closeBySessionKey(sessionKey: string): void { + this.tasks.filter(s => s.sessionKey === sessionKey) + .forEach(s => s.onError(TerminalMessages.sessionClosed)); + } + +}