🎉 重构传输模块.
This commit is contained in:
@@ -41,6 +41,17 @@ export function readFileText(e: File, encoding = 'UTF-8'): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭 fileReader
|
||||
export function closeFileReader(reader: FileReader) {
|
||||
// 清理资源
|
||||
if (reader.readyState === FileReader.LOADING) {
|
||||
reader.abort();
|
||||
}
|
||||
reader.onload = null;
|
||||
reader.onerror = null;
|
||||
reader.onabort = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析路径类型
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<component :is="option.icon" />
|
||||
<!-- 数量 -->
|
||||
<span class="status-count">
|
||||
{{ transferManager.transferList.filter(s => s.status === option.value).length }}
|
||||
{{ transferTasks.filter(s => s.state.status === option.value).length }}
|
||||
</span>
|
||||
</a-tag>
|
||||
</a-space>
|
||||
@@ -48,15 +48,15 @@
|
||||
max-height="100%"
|
||||
:hoverable="true"
|
||||
:bordered="false"
|
||||
:data="transferManager.transferList">
|
||||
:data="transferTasks">
|
||||
<!-- 空数据 -->
|
||||
<template #empty>
|
||||
<a-empty class="list-empty" description="无传输文件" />
|
||||
</template>
|
||||
<!-- 数据 -->
|
||||
<template #item="{ item }">
|
||||
<!-- 传输 item -->
|
||||
<transfer-item v-show="filterItem(item)" :item="item" />
|
||||
<!-- 传输任务 -->
|
||||
<transfer-task v-show="filterItem(item)" :task="item" />
|
||||
</template>
|
||||
</a-list>
|
||||
</a-spin>
|
||||
@@ -70,13 +70,13 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SftpTransferItem } from '@/views/terminal/interfaces';
|
||||
import { ref } from 'vue';
|
||||
import type { IFileTransferTask } from '@/views/terminal/interfaces';
|
||||
import { ref, computed } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import { transferStatusKey } from '../../types/const';
|
||||
import TransferItem from './transfer-item.vue';
|
||||
import TransferTask from './transfer-task.vue';
|
||||
|
||||
const emits = defineEmits(['closed']);
|
||||
|
||||
@@ -87,6 +87,10 @@
|
||||
|
||||
const filterStatus = ref<string>();
|
||||
|
||||
const transferTasks = computed(() => {
|
||||
return [...transferManager.sftp.tasks, ...transferManager.rdp.tasks];
|
||||
});
|
||||
|
||||
// 打开
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
@@ -105,13 +109,14 @@
|
||||
};
|
||||
|
||||
// 过滤传输行
|
||||
const filterItem = (item: SftpTransferItem) => {
|
||||
return !filterStatus.value || item.status === filterStatus.value;
|
||||
const filterItem = (task: IFileTransferTask) => {
|
||||
return !filterStatus.value || task.state.status === filterStatus.value;
|
||||
};
|
||||
|
||||
// 移除全部任务
|
||||
const removeAllTask = () => {
|
||||
transferManager.cancelAllTransfer();
|
||||
transferManager.sftp.cancelAllTransfer();
|
||||
transferManager.rdp.cancelAllTransfer();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -4,46 +4,46 @@
|
||||
<!-- 左侧图标 -->
|
||||
<div class="transfer-item-left">
|
||||
<span class="file-icon">
|
||||
<icon-upload v-if="item.type === TransferType.UPLOAD" />
|
||||
<icon-download v-else-if="item.type === TransferType.DOWNLOAD" />
|
||||
<icon-upload v-if="task.type === TransferType.UPLOAD" />
|
||||
<icon-download v-else-if="task.type === TransferType.DOWNLOAD" />
|
||||
</span>
|
||||
</div>
|
||||
<!-- 中间信息 -->
|
||||
<div class="transfer-item-center">
|
||||
<!-- 文件名称 -->
|
||||
<span class="file-name text-copy"
|
||||
:title="item.name"
|
||||
@click="copy(item.name)">
|
||||
{{ item.name }}
|
||||
:title="task.fileItem.name"
|
||||
@click="copy(task.fileItem.name)">
|
||||
{{ task.fileItem.name }}
|
||||
</span>
|
||||
<!-- 传输进度 -->
|
||||
<span class="transfer-progress">
|
||||
<!-- 当前大小 -->
|
||||
<span v-if="item.status === TransferStatus.TRANSFERRING">{{ getFileSize(item.currentSize) }}</span>
|
||||
<span class="mx4" v-if="item.status === TransferStatus.TRANSFERRING">/</span>
|
||||
<span v-if="task.state.status === TransferStatus.TRANSFERRING && task.fileItem.unknownSize !== true">{{ getFileSize(task.state.currentSize) }}</span>
|
||||
<span class="mx4" v-if="task.state.status === TransferStatus.TRANSFERRING && task.fileItem.unknownSize !== true">/</span>
|
||||
<!-- 总大小 -->
|
||||
<span>{{ getFileSize(item.totalSize) }}</span>
|
||||
<span>{{ getFileSize(task.state.totalSize) }}</span>
|
||||
<!-- 进度百分比 -->
|
||||
<span class="ml8" v-if="item.status === TransferStatus.TRANSFERRING">
|
||||
{{ item.progress }}%
|
||||
<span class="ml8" v-if="task.state.status === TransferStatus.TRANSFERRING && task.fileItem.unknownSize !== true">
|
||||
{{ task.state.progress }}%
|
||||
</span>
|
||||
</span>
|
||||
<!-- 目标目录 -->
|
||||
<span class="target-path text-copy"
|
||||
:title="item.parentPath"
|
||||
@click="copy(item.parentPath)">
|
||||
{{ item.parentPath }}
|
||||
:title="task.fileItem.parentPath"
|
||||
@click="copy(task.fileItem.parentPath)">
|
||||
{{ task.fileItem.parentPath }}
|
||||
</span>
|
||||
<!-- 错误信息 -->
|
||||
<a-tooltip v-if="item.errorMessage"
|
||||
<a-tooltip v-if="task.state.errorMessage"
|
||||
position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
:content="item.errorMessage">
|
||||
:content="task.state.errorMessage">
|
||||
<span class="error-message">
|
||||
{{ item.errorMessage }}
|
||||
{{ task.state.errorMessage }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@@ -52,18 +52,20 @@
|
||||
<!-- 传输状态 -->
|
||||
<div class="transfer-item-right-progress">
|
||||
<!-- 等待传输 -->
|
||||
<icon-clock-circle v-if="item.status === TransferStatus.WAITING" />
|
||||
<icon-clock-circle v-if="task.state.status === TransferStatus.WAITING" />
|
||||
<!-- 传输中-但不知道文件大小 -->
|
||||
<icon-loading v-else-if="task.state.status === TransferStatus.TRANSFERRING && task.fileItem.unknownSize === true" />
|
||||
<!-- 传输进度 -->
|
||||
<a-progress v-else
|
||||
type="circle"
|
||||
size="mini"
|
||||
:status="getDictValue(transferStatusKey, item.status, 'status')"
|
||||
:percent="item.currentSize / item.totalSize" />
|
||||
:status="getDictValue(transferStatusKey, task.state.status, 'status')"
|
||||
:percent="task.state.currentSize / task.state.totalSize" />
|
||||
</div>
|
||||
<!-- 传输操作 -->
|
||||
<div class="transfer-item-right-actions">
|
||||
<!-- 关闭 -->
|
||||
<span class="close-icon" @click="removeTask(item.fileId)">
|
||||
<span class="close-icon" @click="removeTask">
|
||||
<icon-close />
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,21 +76,21 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'transferItem'
|
||||
name: 'transferTask'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SftpTransferItem } from '@/views/terminal/interfaces';
|
||||
import type { IFileTransferTask } from '@/views/terminal/interfaces';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
import { TransferStatus, TransferType, transferStatusKey } from '../../types/const';
|
||||
import { TransferStatus, TransferType, transferStatusKey, TransferSource } from '../../types/const';
|
||||
|
||||
const props = defineProps<{
|
||||
item: SftpTransferItem;
|
||||
task: IFileTransferTask;
|
||||
}>();
|
||||
|
||||
const { transferManager } = useTerminalStore();
|
||||
@@ -97,8 +99,15 @@
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
// 移除任务
|
||||
const removeTask = (fileId: string) => {
|
||||
transferManager.cancelTransfer(fileId);
|
||||
const removeTask = () => {
|
||||
const fileId = props.task.fileId;
|
||||
let manager;
|
||||
if (props.task.source === TransferSource.SFTP) {
|
||||
manager = transferManager.sftp;
|
||||
} else if (props.task.source === TransferSource.RDP) {
|
||||
manager = transferManager.rdp;
|
||||
}
|
||||
manager?.cancelTransfer(fileId);
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -121,8 +121,7 @@
|
||||
// 获取上传的文件
|
||||
const files = fileList.value.map(s => s.file as File);
|
||||
// 普通上传
|
||||
transferManager.addUpload(hostId.value, parentPath.value, files);
|
||||
Message.success('已开始上传, 点击右侧传输列表查看进度');
|
||||
await transferManager.sftp.addUpload(hostId.value, parentPath.value, files);
|
||||
// 清空
|
||||
handlerClear();
|
||||
return true;
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const downloadFiles = (paths: Array<string>, clear: boolean) => {
|
||||
const downloadFiles = async (paths: Array<string>, clear: boolean) => {
|
||||
if (!paths.length) {
|
||||
return;
|
||||
}
|
||||
@@ -172,10 +172,9 @@
|
||||
if (clear) {
|
||||
selectFiles.value = [];
|
||||
}
|
||||
Message.success('已开始下载, 点击右侧传输列表查看进度');
|
||||
// 添加普通文件到下载队列
|
||||
const normalFiles = files.filter(s => !s.isDir);
|
||||
transferManager.addDownload(props.item.hostId as number, currentPath.value, normalFiles);
|
||||
await transferManager.sftp.addDownload(props.item.hostId as number, currentPath.value, normalFiles);
|
||||
// 将文件夹展开普通文件
|
||||
const directoryPaths = files.filter(s => s.isDir).map(s => s.path);
|
||||
if (directoryPaths.length) {
|
||||
@@ -283,7 +282,7 @@
|
||||
if (!checkResult(result, msg)) {
|
||||
return;
|
||||
}
|
||||
transferManager.addDownload(props.item.hostId as number, currentPath, list);
|
||||
transferManager.sftp.addDownload(props.item.hostId as number, currentPath, list);
|
||||
};
|
||||
|
||||
// 初始化会话
|
||||
|
||||
@@ -113,7 +113,7 @@ export interface ITerminalSession<Status extends ReactiveSessionStatus = Reactiv
|
||||
export interface ISshSession extends ITerminalSession, IDomViewportHandler {
|
||||
// terminal 实例
|
||||
inst: Terminal;
|
||||
// 元素对象
|
||||
// 会话配置
|
||||
config: SshInitConfig;
|
||||
// 处理器
|
||||
handler: ISshSessionHandler;
|
||||
@@ -164,7 +164,8 @@ export interface IGuacdSession extends ITerminalSession<GuacdReactiveSessionStat
|
||||
|
||||
// RDP 会话定义
|
||||
export interface IRdpSession extends IGuacdSession {
|
||||
// 元素对象
|
||||
fileSystemName: string;
|
||||
// 会话配置
|
||||
config: GuacdInitConfig;
|
||||
// 视图处理器
|
||||
displayHandler: IRdpSessionDisplayHandler;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { FileTransferItem, FileTransferReactiveState, IFileTransferTask } from '@/views/terminal/interfaces';
|
||||
import type { Reactive } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
import { nextId } from '@/utils';
|
||||
import { TransferStatus } from '@/views/terminal/types/const';
|
||||
|
||||
// 文件传输任务基类
|
||||
export default abstract class BaseFileTransferTask implements IFileTransferTask {
|
||||
public type: string;
|
||||
public source: string;
|
||||
public fileId: string;
|
||||
public hostId: number;
|
||||
public sessionKey: string;
|
||||
// 文件
|
||||
public fileItem: FileTransferItem;
|
||||
// 状态
|
||||
public state: Reactive<FileTransferReactiveState>;
|
||||
|
||||
protected constructor(type: string, source: string,
|
||||
hostId: number, sessionKey: string,
|
||||
fileItem: FileTransferItem,
|
||||
state: Partial<FileTransferReactiveState>) {
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
this.fileId = nextId(10);
|
||||
this.hostId = hostId;
|
||||
this.sessionKey = sessionKey;
|
||||
this.fileItem = fileItem;
|
||||
this.state = reactive({
|
||||
currentSize: 0,
|
||||
totalSize: fileItem.size,
|
||||
progress: 0,
|
||||
status: TransferStatus.WAITING,
|
||||
errorMessage: undefined,
|
||||
finished: false,
|
||||
aborted: false,
|
||||
...state,
|
||||
});
|
||||
}
|
||||
|
||||
// 开始
|
||||
abstract start(): void;
|
||||
|
||||
// 完成
|
||||
abstract finish(): void;
|
||||
|
||||
// 失败
|
||||
abstract error(): void;
|
||||
|
||||
// 中断
|
||||
abstract abort(): void;
|
||||
|
||||
// 传输完成回调
|
||||
abstract onFinish(): void;
|
||||
|
||||
// 传输失败回调
|
||||
abstract onError(msg: string | undefined): void;
|
||||
|
||||
// 传输中断回调
|
||||
abstract onAbort(): void;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { ISetTransferClient, FileTransferItem } from '@/views/terminal/interfaces';
|
||||
import { TransferOperator, TransferStatus, TerminalMessages, TransferSource } from '../../types/const';
|
||||
import { getPath } from '@/utils/file';
|
||||
import BaseFileTransferTask from './base-file-transfer-task';
|
||||
|
||||
// sftp 传输任务一定义
|
||||
export default abstract class SftpBaseTransferTask extends BaseFileTransferTask implements ISetTransferClient<WebSocket> {
|
||||
|
||||
protected client?: WebSocket;
|
||||
|
||||
protected constructor(type: string,
|
||||
hostId: number,
|
||||
fileItem: FileTransferItem) {
|
||||
super(type, TransferSource.SFTP, hostId, undefined as unknown as string, fileItem, {});
|
||||
}
|
||||
|
||||
// 设置传输客户端
|
||||
setClient(client: WebSocket) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
// 开始
|
||||
start() {
|
||||
this.state.status = TransferStatus.TRANSFERRING;
|
||||
// 发送开始信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.START,
|
||||
type: this.type,
|
||||
hostId: this.hostId,
|
||||
path: getPath(this.fileItem.parentPath + '/' + this.fileItem.name),
|
||||
paths: this.fileItem.paths,
|
||||
}));
|
||||
};
|
||||
|
||||
// 完成
|
||||
finish() {
|
||||
this.state.finished = true;
|
||||
this.state.progress = 100;
|
||||
this.state.status = TransferStatus.SUCCESS;
|
||||
// 发送完成的信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.FINISH,
|
||||
type: this.type,
|
||||
hostId: this.hostId,
|
||||
}));
|
||||
};
|
||||
|
||||
// 失败
|
||||
error() {
|
||||
this.state.finished = true;
|
||||
this.state.status = TransferStatus.ERROR;
|
||||
// 发送上传失败的信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.ERROR,
|
||||
type: this.type,
|
||||
hostId: this.hostId,
|
||||
}));
|
||||
};
|
||||
|
||||
// 中断
|
||||
abort() {
|
||||
this.state.aborted = true;
|
||||
// 发送中断的信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.ABORT,
|
||||
type: this.type,
|
||||
hostId: this.hostId,
|
||||
}));
|
||||
}
|
||||
|
||||
// 失败回调
|
||||
onError(msg: string | undefined) {
|
||||
this.state.finished = true;
|
||||
this.state.status = TransferStatus.ERROR;
|
||||
this.state.errorMessage = msg || TerminalMessages.fileTransferError;
|
||||
}
|
||||
|
||||
// 完成回调
|
||||
onFinish() {
|
||||
this.state.finished = true;
|
||||
this.state.progress = 100;
|
||||
this.state.status = TransferStatus.SUCCESS;
|
||||
this.state.currentSize = this.state.totalSize;
|
||||
};
|
||||
|
||||
// 中断回调
|
||||
onAbort() {
|
||||
this.state.aborted = true;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { FileTransferItem, IFileDownloadTask } from '@/views/terminal/interfaces';
|
||||
import { TransferStatus, TerminalMessages } from '../../types/const';
|
||||
import { getFileName, openDownloadFile } from '@/utils/file';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { getDownloadTransferUrl } from '@/api/terminal/terminal-sftp';
|
||||
import SftpBaseTransferTask from './sftp-base-transfer-task';
|
||||
|
||||
// sftp 下载任务实现
|
||||
export default class SftpFileDownloadTask extends SftpBaseTransferTask implements IFileDownloadTask {
|
||||
|
||||
constructor(type: string, hostId: number, fileItem: FileTransferItem) {
|
||||
super(type, hostId, fileItem);
|
||||
}
|
||||
|
||||
// 开始回调
|
||||
onStart(channelId: string, token: string) {
|
||||
// 获取下载 url
|
||||
const url = getDownloadTransferUrl(channelId, token);
|
||||
// 打开
|
||||
openDownloadFile(url);
|
||||
}
|
||||
|
||||
// 进度回调
|
||||
onProgress(totalSize: number | undefined, currentSize: number | undefined) {
|
||||
if (totalSize) {
|
||||
this.state.totalSize = totalSize;
|
||||
}
|
||||
if (currentSize) {
|
||||
this.state.currentSize = currentSize;
|
||||
}
|
||||
};
|
||||
|
||||
// 完成回调
|
||||
onFinish() {
|
||||
super.onFinish();
|
||||
if (this.state.aborted) {
|
||||
// 中断则不触发下载
|
||||
return;
|
||||
}
|
||||
if (this.state.totalSize === 0) {
|
||||
// 空文件直接触发下载
|
||||
try {
|
||||
// 触发下载
|
||||
saveAs(new Blob([], {
|
||||
type: 'application/octet-stream'
|
||||
}), getFileName(this.fileItem.name));
|
||||
this.state.status = TransferStatus.SUCCESS;
|
||||
} catch (e) {
|
||||
this.state.status = TransferStatus.ERROR;
|
||||
this.state.errorMessage = TerminalMessages.fileSaveError;
|
||||
}
|
||||
} else {
|
||||
this.state.status = TransferStatus.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { FileTransferItem, IFileUploadTask } from '@/views/terminal/interfaces';
|
||||
import { closeFileReader } from '@/utils/file';
|
||||
import SftpBaseTransferTask from './sftp-base-transfer-task';
|
||||
|
||||
// 512 KB
|
||||
export const PART_SIZE = 512 * 1024;
|
||||
|
||||
// sftp 上传任务实现
|
||||
export default class SftpFileUploadTask extends SftpBaseTransferTask implements IFileUploadTask {
|
||||
|
||||
private currentPart: number;
|
||||
private readonly totalPart: number;
|
||||
|
||||
constructor(type: string, hostId: number, fileItem: FileTransferItem) {
|
||||
super(type, hostId, fileItem);
|
||||
this.currentPart = 0;
|
||||
this.totalPart = Math.ceil(fileItem.size / PART_SIZE);
|
||||
}
|
||||
|
||||
// 上传完成
|
||||
finish() {
|
||||
super.finish();
|
||||
// 释放资源
|
||||
this.fileItem.file = undefined;
|
||||
}
|
||||
|
||||
// 上传下一个分片
|
||||
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.client?.send(arrayBuffer as ArrayBuffer);
|
||||
this.currentPart++;
|
||||
this.state.currentSize += (end - start);
|
||||
} finally {
|
||||
// 释放资源
|
||||
closeFileReader(reader);
|
||||
}
|
||||
}
|
||||
|
||||
// 是否有下一个分片
|
||||
private hasNextPart() {
|
||||
return this.currentPart < this.totalPart;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { SftpTransferItem } from '@/views/terminal/interfaces';
|
||||
import { TransferStatus } from '../../types/const';
|
||||
import { getFileName, openDownloadFile } from '@/utils/file';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { getDownloadTransferUrl } from '@/api/terminal/terminal-sftp';
|
||||
import SftpTransferHandler from './sftp-transfer-handler';
|
||||
|
||||
// sftp 下载器实现
|
||||
export default class SftpTransferDownloader extends SftpTransferHandler {
|
||||
|
||||
constructor(type: string, item: SftpTransferItem, client: WebSocket) {
|
||||
super(type, item, client);
|
||||
}
|
||||
|
||||
// 开始回调
|
||||
onStart(channelId: string, token: string) {
|
||||
super.onStart(channelId, token);
|
||||
// 获取下载 url
|
||||
const url = getDownloadTransferUrl(channelId, token);
|
||||
// 打开
|
||||
openDownloadFile(url);
|
||||
}
|
||||
|
||||
// 完成回调
|
||||
onFinish() {
|
||||
super.onFinish();
|
||||
if (this.aborted) {
|
||||
// 中断则不触发下载
|
||||
return;
|
||||
}
|
||||
if (this.item.totalSize === 0) {
|
||||
// 空文件直接触发下载
|
||||
try {
|
||||
// 触发下载
|
||||
saveAs(new Blob([], {
|
||||
type: 'application/octet-stream'
|
||||
}), getFileName(this.item.name));
|
||||
this.item.status = TransferStatus.SUCCESS;
|
||||
} catch (e) {
|
||||
this.item.status = TransferStatus.ERROR;
|
||||
this.item.errorMessage = '保存失败';
|
||||
}
|
||||
} else {
|
||||
this.item.status = TransferStatus.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import type { ISftpTransferHandler, SftpTransferItem } from '@/views/terminal/interfaces';
|
||||
import { TransferOperator, TransferStatus } from '../../types/const';
|
||||
import { getPath } from '@/utils/file';
|
||||
|
||||
// sftp 传输处理器定义
|
||||
export default abstract class SftpTransferHandler implements ISftpTransferHandler {
|
||||
|
||||
public type: string;
|
||||
public finished: boolean;
|
||||
public aborted: boolean;
|
||||
protected client: WebSocket;
|
||||
protected item: SftpTransferItem;
|
||||
|
||||
protected constructor(type: string, item: SftpTransferItem, client: WebSocket) {
|
||||
this.type = type;
|
||||
this.finished = false;
|
||||
this.aborted = false;
|
||||
this.item = item;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
// 开始
|
||||
start() {
|
||||
this.item.status = TransferStatus.TRANSFERRING;
|
||||
// 发送开始信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.START,
|
||||
type: this.type,
|
||||
path: getPath(this.item.parentPath + '/' + this.item.name),
|
||||
hostId: this.item.hostId,
|
||||
paths: this.item.paths,
|
||||
}));
|
||||
};
|
||||
|
||||
// 完成
|
||||
finish() {
|
||||
this.finished = true;
|
||||
this.item.status = TransferStatus.SUCCESS;
|
||||
// 发送完成的信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.FINISH,
|
||||
type: this.type,
|
||||
hostId: this.item.hostId
|
||||
}));
|
||||
};
|
||||
|
||||
// 失败
|
||||
error() {
|
||||
this.finished = true;
|
||||
this.item.status = TransferStatus.ERROR;
|
||||
// 发送上传失败的信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.ERROR,
|
||||
type: this.type,
|
||||
hostId: this.item.hostId
|
||||
}));
|
||||
};
|
||||
|
||||
// 中断
|
||||
abort() {
|
||||
this.aborted = true;
|
||||
// 发送中断的信息
|
||||
this.client?.send(JSON.stringify({
|
||||
operator: TransferOperator.ABORT,
|
||||
type: this.type,
|
||||
hostId: this.item.hostId
|
||||
}));
|
||||
}
|
||||
|
||||
// 是否有下一个分片
|
||||
hasNextPart() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// 下一页分片回调
|
||||
onNextPart() {
|
||||
return undefined as unknown as any;
|
||||
};
|
||||
|
||||
// 开始回调
|
||||
onStart(channelId: string, token: string) {
|
||||
};
|
||||
|
||||
// 进度回调
|
||||
onProgress(totalSize: number | undefined, currentSize: number | undefined) {
|
||||
if (this.item) {
|
||||
if (totalSize) {
|
||||
this.item.totalSize = totalSize;
|
||||
}
|
||||
if (currentSize) {
|
||||
this.item.currentSize = currentSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 失败回调
|
||||
onError(msg: string | undefined) {
|
||||
this.finished = true;
|
||||
this.item.status = TransferStatus.ERROR;
|
||||
this.item.errorMessage = msg || '传输失败';
|
||||
}
|
||||
|
||||
// 完成回调
|
||||
onFinish() {
|
||||
this.finished = true;
|
||||
this.item.status = TransferStatus.SUCCESS;
|
||||
this.item.currentSize = this.item.totalSize;
|
||||
};
|
||||
|
||||
// 中断回调
|
||||
onAbort() {
|
||||
this.aborted = true;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,105 +1,89 @@
|
||||
import type { ISftpTransferHandler, ISftpTransferManager, SftpFile, SftpTransferItem, TransferOperatorResponse } from '@/views/terminal/interfaces';
|
||||
import type {
|
||||
FileTransferTaskType,
|
||||
ISetTransferClient,
|
||||
ISftpTransferManager,
|
||||
MaybeFileTransferTask,
|
||||
SftpFile,
|
||||
TransferOperatorResponse
|
||||
} from '@/views/terminal/interfaces';
|
||||
import { TerminalMessages, TransferReceiver, TransferStatus, TransferType } from '../../types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getTerminalTransferToken, openTerminalTransferChannel } from '@/api/terminal/terminal';
|
||||
import { nextId } from '@/utils';
|
||||
import SftpTransferUploader from './sftp-transfer-uploader';
|
||||
import SftpTransferDownloader from './sftp-transfer-downloader';
|
||||
import BaseTransferManager from './base-transfer-manager';
|
||||
import SftpFileUploadTask from './sftp-file-upload-task';
|
||||
import SftpFileDownloadTask from './sftp-file-download-task';
|
||||
|
||||
// sftp 传输管理器实现
|
||||
export default class SftpTransferManager implements ISftpTransferManager {
|
||||
export default class SftpTransferManager extends BaseTransferManager implements ISftpTransferManager {
|
||||
|
||||
private client?: WebSocket;
|
||||
|
||||
private run: boolean;
|
||||
|
||||
private progressIntervalId?: any;
|
||||
|
||||
private currentItem?: SftpTransferItem;
|
||||
|
||||
private currentTransfer?: ISftpTransferHandler;
|
||||
|
||||
public transferList: Array<SftpTransferItem>;
|
||||
private currentTask?: FileTransferTaskType;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.run = false;
|
||||
this.transferList = [];
|
||||
}
|
||||
|
||||
// 添加上传任务
|
||||
addUpload(hostId: number, parentPath: string, files: Array<File>) {
|
||||
// 转为上传任务
|
||||
const items = files.map(s => {
|
||||
return {
|
||||
fileId: nextId(10),
|
||||
type: TransferType.UPLOAD,
|
||||
hostId: hostId,
|
||||
name: s.webkitRelativePath || s.name,
|
||||
currentSize: 0,
|
||||
totalSize: s.size,
|
||||
progress: 0,
|
||||
status: TransferStatus.WAITING,
|
||||
async addUpload(hostId: number, parentPath: string, files: Array<File>) {
|
||||
Message.info(TerminalMessages.fileUploading);
|
||||
// 创建任务
|
||||
for (let file of files) {
|
||||
const task = new SftpFileUploadTask(TransferType.UPLOAD, hostId, {
|
||||
name: file.webkitRelativePath || file.name,
|
||||
parentPath: parentPath,
|
||||
file: s
|
||||
};
|
||||
});
|
||||
size: file.size,
|
||||
file,
|
||||
});
|
||||
this.tasks.push(task);
|
||||
}
|
||||
// 开始传输
|
||||
this.startTransfer(items);
|
||||
await this.startTransfer();
|
||||
}
|
||||
|
||||
// 添加下载任务
|
||||
addDownload(hostId: number, currentPath: string, files: Array<SftpFile>) {
|
||||
async addDownload(hostId: number, currentPath: string, files: Array<SftpFile>) {
|
||||
Message.info(TerminalMessages.fileDownloading);
|
||||
let pathIndex = currentPath === '/' ? 1 : currentPath.length + 1;
|
||||
// 转为下载文件
|
||||
const items = files.map(s => {
|
||||
return {
|
||||
fileId: nextId(10),
|
||||
type: TransferType.DOWNLOAD,
|
||||
hostId: hostId,
|
||||
name: s.path.substring(pathIndex),
|
||||
for (let file of files) {
|
||||
// 创建任务
|
||||
const task = new SftpFileDownloadTask(TransferType.DOWNLOAD, hostId, {
|
||||
name: file.path.substring(pathIndex),
|
||||
parentPath: currentPath,
|
||||
currentSize: 0,
|
||||
totalSize: s.size,
|
||||
progress: 0,
|
||||
status: TransferStatus.WAITING,
|
||||
};
|
||||
}) as Array<SftpTransferItem>;
|
||||
size: file.size,
|
||||
});
|
||||
this.tasks.push(task);
|
||||
}
|
||||
// 开始传输
|
||||
this.startTransfer(items);
|
||||
await this.startTransfer();
|
||||
}
|
||||
|
||||
// 开始传输
|
||||
private startTransfer(items: Array<SftpTransferItem>) {
|
||||
this.transferList.push(...items);
|
||||
private async startTransfer() {
|
||||
// 开始传输
|
||||
if (!this.run) {
|
||||
this.openClient();
|
||||
await this.openClient();
|
||||
}
|
||||
// 开始计算进度
|
||||
this.resetProgressTimer();
|
||||
}
|
||||
|
||||
// 取消传输
|
||||
cancelTransfer(fileId: string): void {
|
||||
const index = this.transferList.findIndex(s => s.fileId === fileId);
|
||||
const index = this.tasks.findIndex(s => s.fileId === fileId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
const item = this.transferList[index];
|
||||
if (item.status === TransferStatus.TRANSFERRING) {
|
||||
const task = this.tasks[index];
|
||||
if (task.state.status === TransferStatus.TRANSFERRING) {
|
||||
// 传输中则中断传输
|
||||
this.currentTransfer?.abort();
|
||||
this.currentTask?.abort();
|
||||
}
|
||||
// 从列表中移除
|
||||
this.transferList.splice(index, 1);
|
||||
}
|
||||
|
||||
// 取消全部传输
|
||||
cancelAllTransfer(): void {
|
||||
// 从列表中移除非传输中的元素
|
||||
this.transferList.reduceRight((_, value: SftpTransferItem, index: number) => {
|
||||
if (value.status !== TransferStatus.TRANSFERRING) {
|
||||
this.transferList.splice(index, 1);
|
||||
}
|
||||
}, null as any);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
|
||||
// 打开会话
|
||||
@@ -115,11 +99,10 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
Message.error('会话打开失败');
|
||||
console.error('transfer error', e);
|
||||
// 将等待中和传输中任务修改为失败状态
|
||||
this.transferList.filter(s => {
|
||||
return s.status === TransferStatus.WAITING
|
||||
|| s.status === TransferStatus.TRANSFERRING;
|
||||
this.tasks.filter(s => {
|
||||
return s.state.status === TransferStatus.WAITING || s.state.status === TransferStatus.TRANSFERRING;
|
||||
}).forEach(s => {
|
||||
s.status = TransferStatus.ERROR;
|
||||
s.state.status = TransferStatus.ERROR;
|
||||
});
|
||||
// 关闭会话
|
||||
this.close();
|
||||
@@ -132,100 +115,62 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
};
|
||||
// 处理消息
|
||||
this.client.onmessage = this.resolveMessage.bind(this);
|
||||
// 计算传输进度
|
||||
this.progressIntervalId = setInterval(this.calcProgress.bind(this), 500);
|
||||
// 打开后自动传输下一个任务
|
||||
this.transferNextItem();
|
||||
}
|
||||
|
||||
// 计算传输进度
|
||||
private calcProgress() {
|
||||
this.transferList.forEach(item => {
|
||||
if (item.totalSize != 0) {
|
||||
item.progress = (item.currentSize / item.totalSize * 100).toFixed(2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 传输下一条任务
|
||||
private transferNextItem() {
|
||||
this.currentTransfer = undefined;
|
||||
// 释放内存
|
||||
if (this.currentItem) {
|
||||
this.currentItem.file = null as unknown as File;
|
||||
}
|
||||
// 获取任务
|
||||
this.currentItem = this.transferList.find(s => s.status === TransferStatus.WAITING);
|
||||
if (this.currentItem) {
|
||||
// 创建传输器
|
||||
this.currentTransfer = this.createTransfer();
|
||||
this.currentTask = this.tasks.find(s => s.state.status === TransferStatus.WAITING);
|
||||
if (this.currentTask) {
|
||||
// 设置 client
|
||||
(this.currentTask as unknown as ISetTransferClient<WebSocket>).setClient(this.client as WebSocket);
|
||||
// 开始
|
||||
this.currentTransfer?.start();
|
||||
this.currentTask?.start();
|
||||
} else {
|
||||
// 无任务关闭会话
|
||||
this.client?.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建传输器
|
||||
private createTransfer(): ISftpTransferHandler | undefined {
|
||||
if (!this.currentItem) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.currentItem.type === TransferType.UPLOAD) {
|
||||
// 上传
|
||||
return new SftpTransferUploader(TransferType.UPLOAD, this.currentItem, this.client as WebSocket);
|
||||
} else if (this.currentItem.type === TransferType.DOWNLOAD) {
|
||||
// 下载
|
||||
return new SftpTransferDownloader(TransferType.DOWNLOAD, this.currentItem, this.client as WebSocket);
|
||||
}
|
||||
}
|
||||
|
||||
// 接收消息
|
||||
private async resolveMessage(message: MessageEvent) {
|
||||
// 文本消息
|
||||
const data = JSON.parse(message.data) as TransferOperatorResponse;
|
||||
if (data.type === TransferReceiver.NEXT_PART) {
|
||||
// 接收下一块数据回调
|
||||
await this.currentTransfer?.onNextPart();
|
||||
await (this.currentTask as MaybeFileTransferTask)?.onNextPart?.();
|
||||
} else if (data.type === TransferReceiver.START) {
|
||||
// 开始回调
|
||||
this.currentTransfer?.onStart(data.channelId as string, data.transferToken as string);
|
||||
// 开始下载回调
|
||||
(this.currentTask as MaybeFileTransferTask)?.onStart?.(data.channelId as string, data.transferToken as string);
|
||||
} else if (data.type === TransferReceiver.PROGRESS) {
|
||||
// 进度回调
|
||||
this.currentTransfer?.onProgress(data.totalSize, data.currentSize);
|
||||
// 下载进度回调
|
||||
(this.currentTask as MaybeFileTransferTask)?.onProgress?.(data.totalSize, data.currentSize);
|
||||
} else if (data.type === TransferReceiver.FINISH) {
|
||||
// 完成回调
|
||||
this.currentTransfer?.onFinish();
|
||||
this.currentTask?.onFinish();
|
||||
// 开始下一个传输任务
|
||||
this.transferNextItem();
|
||||
} else if (data.type === TransferReceiver.ERROR) {
|
||||
// 失败回调
|
||||
this.currentTransfer?.onError(data.msg);
|
||||
this.currentTask?.onError(data.msg);
|
||||
// 开始下一个传输任务
|
||||
this.transferNextItem();
|
||||
} else if (data.type === TransferReceiver.ABORT) {
|
||||
// 中断回调
|
||||
this.currentTransfer?.onAbort();
|
||||
this.currentTask?.onAbort();
|
||||
// 开始下一个传输任务
|
||||
this.transferNextItem();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 释放资源
|
||||
private close() {
|
||||
protected close() {
|
||||
// 重置 run
|
||||
this.run = false;
|
||||
// 关闭传输进度
|
||||
clearInterval(this.progressIntervalId);
|
||||
// 进行中和等待中的文件改为失败
|
||||
this.transferList.forEach(s => {
|
||||
if (s.status === TransferStatus.WAITING ||
|
||||
s.status === TransferStatus.TRANSFERRING) {
|
||||
s.status = TransferStatus.ERROR;
|
||||
s.errorMessage = TerminalMessages.sessionClosed;
|
||||
}
|
||||
});
|
||||
// 关闭
|
||||
super.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { SftpTransferItem } from '@/views/terminal/interfaces';
|
||||
import SftpTransferHandler from './sftp-transfer-handler';
|
||||
|
||||
// 512 KB
|
||||
export const PART_SIZE = 512 * 1024;
|
||||
|
||||
// sftp 上传器实现
|
||||
export default class SftpTransferUploader extends SftpTransferHandler {
|
||||
|
||||
private currentPart: number;
|
||||
private readonly totalPart: number;
|
||||
private file: File;
|
||||
|
||||
constructor(type: string, item: SftpTransferItem, client: WebSocket) {
|
||||
super(type, item, client);
|
||||
this.file = item.file;
|
||||
this.currentPart = 0;
|
||||
this.totalPart = Math.ceil(item.file.size / PART_SIZE);
|
||||
}
|
||||
|
||||
// 是否有下一个分片
|
||||
hasNextPart() {
|
||||
return this.currentPart < this.totalPart;
|
||||
}
|
||||
|
||||
// 上传下一个分片
|
||||
async onNextPart() {
|
||||
super.onNextPart();
|
||||
// 完成或者中断直接跳过
|
||||
if (this.aborted || this.finished) {
|
||||
return;
|
||||
}
|
||||
if (this.hasNextPart()) {
|
||||
try {
|
||||
// 有下一个分片则上传
|
||||
await this.doUploadNextPart();
|
||||
} catch (e) {
|
||||
// 读取文件失败
|
||||
this.error();
|
||||
}
|
||||
} else {
|
||||
this.finish();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行上传下一分片
|
||||
private async doUploadNextPart() {
|
||||
// 读取数据
|
||||
const start = this.currentPart * PART_SIZE;
|
||||
const end = Math.min(this.file.size, start + PART_SIZE);
|
||||
const chunk = this.file.slice(start, end);
|
||||
const reader = new FileReader();
|
||||
const arrayBuffer = await new Promise((resolve, reject) => {
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(chunk);
|
||||
});
|
||||
// 发送数据
|
||||
this.client?.send(arrayBuffer as ArrayBuffer);
|
||||
this.currentPart++;
|
||||
this.item.currentSize += (end - start);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -98,6 +98,10 @@ export const TerminalMessages = {
|
||||
waitingReconnect: '输入回车重新连接...',
|
||||
loggedElsewhere: '该账号已在另一台设备登录',
|
||||
rdpConnectTimeout: '请检查远程计算机网络及其他配置是否正常',
|
||||
fileTransferError: '传输失败',
|
||||
fileSaveError: '保存失败',
|
||||
fileUploading: '已开始上传, 点击右侧传输列表查看进度',
|
||||
fileDownloading: '已开始下载, 点击右侧传输列表查看进度',
|
||||
};
|
||||
|
||||
// 文件类型
|
||||
@@ -457,6 +461,12 @@ export const TransferType = {
|
||||
DOWNLOAD: 'download',
|
||||
};
|
||||
|
||||
// 传输来源
|
||||
export const TransferSource = {
|
||||
SFTP: 'sftp',
|
||||
RDP: 'rdp',
|
||||
};
|
||||
|
||||
// 传输操作
|
||||
export const TransferOperator = {
|
||||
START: 'start',
|
||||
|
||||
Reference in New Issue
Block a user