🎉 重构传输模块.

This commit is contained in:
lijiahangmax
2025-06-28 01:23:53 +08:00
parent 49a0b6786e
commit 9a7cfc4061
15 changed files with 427 additions and 391 deletions

View File

@@ -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;
}
/**
* 解析路径类型
*/

View File

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

View File

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

View File

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

View File

@@ -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);
};
// 初始化会话

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',