🔨 添加 RDP 上传功能.
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 终端配置
|
||||
|
||||
@@ -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 = [];
|
||||
};
|
||||
|
||||
// 选择文件回调
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 传输操作响应
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user