🔨 添加 RDP 上传功能.

This commit is contained in:
lijiahangmax
2025-06-28 01:25:54 +08:00
parent 9a7cfc4061
commit 1328894188
7 changed files with 407 additions and 54 deletions

View File

@@ -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: {

View File

@@ -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;
}
// 终端配置

View File

@@ -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 = [];
};
// 选择文件回调

View File

@@ -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<string>;
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<Reactive<FileTransferTaskType>>;
// sftp 传输管理器定义
export interface ISftpTransferManager {
transferList: Array<SftpTransferItem>;
// 添加上传任务
addUpload: (hostId: number, parentPath: string, files: Array<File>) => void;
// 添加下载任务
addDownload: (hostId: number, currentPath: string, files: Array<SftpFile>) => void;
// 取消传输
cancelTransfer: (fileId: string) => void;
// 取消全部传输
// 取消全部执行中的传输
cancelAllTransfer: () => void;
}
// sftp 传输处理回调定义
export interface ISftpTransferCallback {
// 下一分片回调
onNextPart: () => Promise<void>;
// 开始回调
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<File>) => Promise<void>;
// 添加下载任务
addDownload: (hostId: number, currentPath: string, files: Array<SftpFile>) => Promise<void>;
}
// sftp 传输理器定义
export interface ISftpTransferHandler extends ISftpTransferCallback {
// 类型
// RDP 文件传输理器定义
export interface IRdpTransferManager extends ITransferManager {
// 添加上传任务
addUpload: (session: IRdpSession, file: File) => Promise<void>;
// 添加下载任务
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<T> {
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<FileTransferReactiveState>;
// 开始
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<string>;
// 文件上传任务定义
export interface IFileUploadTask extends IFileTransferTask {
// 请求上传下一个分片
onNextPart: () => Promise<void>;
}
// 文件下载任务定义
export interface IFileDownloadTask extends IFileTransferTask {
// 开始下载回调
onStart: (channelId: string, token: string) => void;
// 下载进度回调
onProgress: (totalSize: number | undefined, currentSize: number | undefined) => void;
}
// 传输操作响应

View File

@@ -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<FileTransferTaskType>;
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;
}
});
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}