优化上传逻辑.

This commit is contained in:
lijiahang
2024-02-22 16:31:45 +08:00
parent 5bd49d97f7
commit 1711981d80
11 changed files with 277 additions and 90 deletions

View File

@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
/** /**
* 消息操作类型 * 传输操作类型
* *
* @author Jiahang Li * @author Jiahang Li
* @version 1.0.0 * @version 1.0.0
@@ -14,20 +14,20 @@ import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
public enum TransferOperatorType { public enum TransferOperatorType {
/**
* 处理完成
*/
PROCESSED("processed"),
/** /**
* 开始上传 * 开始上传
*/ */
UPLOAD_START("upload_start"), UPLOAD_START("uploadStart"),
/** /**
* 上传完成 * 上传完成
*/ */
UPLOAD_FINISH("upload_finish"), UPLOAD_FINISH("uploadFinish"),
/**
* 上传失败
*/
UPLOAD_ERROR("uploadError"),
; ;

View File

@@ -0,0 +1,43 @@
package com.orion.ops.module.asset.handler.host.transfer.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 传输响应类型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/2/21 22:03
*/
@Getter
@AllArgsConstructor
public enum TransferReceiverType {
/**
* 请求下一块上传数据
*/
NEXT_BLOCK("nextBlock"),
/**
* 请求下一个传输任务
*/
NEXT_TRANSFER("nextTransfer"),
;
private final String type;
public static TransferReceiverType of(String type) {
if (type == null) {
return null;
}
for (TransferReceiverType value : values()) {
if (value.type.equals(type)) {
return value;
}
}
return null;
}
}

View File

@@ -4,12 +4,14 @@ import com.alibaba.fastjson.JSON;
import com.orion.lang.exception.argument.InvalidArgumentException; import com.orion.lang.exception.argument.InvalidArgumentException;
import com.orion.lang.utils.io.Streams; import com.orion.lang.utils.io.Streams;
import com.orion.net.host.SessionStore; import com.orion.net.host.SessionStore;
import com.orion.ops.framework.common.constant.Const;
import com.orion.ops.framework.common.constant.ErrorMessage; import com.orion.ops.framework.common.constant.ErrorMessage;
import com.orion.ops.framework.common.constant.ExtraFieldConst; import com.orion.ops.framework.common.constant.ExtraFieldConst;
import com.orion.ops.framework.websocket.core.utils.WebSockets; import com.orion.ops.framework.websocket.core.utils.WebSockets;
import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO; import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
import com.orion.ops.module.asset.enums.HostConnectTypeEnum; import com.orion.ops.module.asset.enums.HostConnectTypeEnum;
import com.orion.ops.module.asset.handler.host.transfer.enums.TransferOperatorType; import com.orion.ops.module.asset.handler.host.transfer.enums.TransferOperatorType;
import com.orion.ops.module.asset.handler.host.transfer.enums.TransferReceiverType;
import com.orion.ops.module.asset.handler.host.transfer.model.TransferOperatorRequest; import com.orion.ops.module.asset.handler.host.transfer.model.TransferOperatorRequest;
import com.orion.ops.module.asset.handler.host.transfer.model.TransferOperatorResponse; import com.orion.ops.module.asset.handler.host.transfer.model.TransferOperatorResponse;
import com.orion.ops.module.asset.handler.host.transfer.session.ITransferHostSession; import com.orion.ops.module.asset.handler.host.transfer.session.ITransferHostSession;
@@ -72,6 +74,10 @@ public class TransferHandler implements ITransferHandler {
// 上传完成 // 上传完成
this.uploadFinish(); this.uploadFinish();
break; break;
case UPLOAD_ERROR:
// 上传失败
this.uploadError();
break;
default: default:
break; break;
} }
@@ -79,23 +85,18 @@ public class TransferHandler implements ITransferHandler {
@Override @Override
public void putContent(byte[] content) { public void putContent(byte[] content) {
Exception ex = null;
try { try {
// 写入内容 // 写入内容
currentSession.putContent(content); currentSession.putContent(content);
// 响应结果
this.sendMessage(TransferReceiverType.NEXT_BLOCK, null);
} catch (IOException e) { } catch (IOException e) {
ex = e;
log.error("TransferHandler.putContent error", e); log.error("TransferHandler.putContent error", e);
// 写入完成 // 写入完成
currentSession.putFinish(); currentSession.putFinish();
// 响应结果
this.sendMessage(TransferReceiverType.NEXT_TRANSFER, e);
} }
// 响应结果
TransferOperatorResponse resp = TransferOperatorResponse.builder()
.type(TransferOperatorType.PROCESSED.getType())
.success(ex == null)
.msg(this.getErrorMessage(ex))
.build();
WebSockets.sendText(this.channel, JSON.toJSONString(resp));
} }
/** /**
@@ -104,20 +105,18 @@ public class TransferHandler implements ITransferHandler {
* @param payload payload * @param payload payload
*/ */
private void uploadStart(TransferOperatorRequest payload) { private void uploadStart(TransferOperatorRequest payload) {
Exception ex = null;
try { try {
// 开始上传
currentSession.startUpload(payload.getPath()); currentSession.startUpload(payload.getPath());
// 响应结果
this.sendMessage(TransferReceiverType.NEXT_BLOCK, null);
} catch (Exception e) { } catch (Exception e) {
ex = e;
log.error("TransferHandler.uploadStart error", e); log.error("TransferHandler.uploadStart error", e);
// 传输完成
currentSession.putFinish();
// 响应结果
this.sendMessage(TransferReceiverType.NEXT_TRANSFER, e);
} }
// 响应结果
TransferOperatorResponse resp = TransferOperatorResponse.builder()
.type(TransferOperatorType.PROCESSED.getType())
.success(ex == null)
.msg(this.getErrorMessage(ex))
.build();
WebSockets.sendText(this.channel, JSON.toJSONString(resp));
} }
/** /**
@@ -126,11 +125,16 @@ public class TransferHandler implements ITransferHandler {
private void uploadFinish() { private void uploadFinish() {
currentSession.putFinish(); currentSession.putFinish();
// 响应结果 // 响应结果
TransferOperatorResponse resp = TransferOperatorResponse.builder() this.sendMessage(TransferReceiverType.NEXT_TRANSFER, null);
.type(TransferOperatorType.PROCESSED.getType()) }
.success(true)
.build(); /**
WebSockets.sendText(this.channel, JSON.toJSONString(resp)); * 上传失败
*/
private void uploadError() {
currentSession.putFinish();
// 响应结果
this.sendMessage(TransferReceiverType.NEXT_TRANSFER, new InvalidArgumentException(Const.EMPTY));
} }
/** /**
@@ -158,16 +162,26 @@ public class TransferHandler implements ITransferHandler {
} catch (Exception e) { } catch (Exception e) {
log.error("TransferHandler.getAndInitSession error", e); log.error("TransferHandler.getAndInitSession error", e);
// 响应结果 // 响应结果
TransferOperatorResponse resp = TransferOperatorResponse.builder() this.sendMessage(TransferReceiverType.NEXT_TRANSFER, e);
.type(TransferOperatorType.PROCESSED.getType())
.success(false)
.msg(this.getErrorMessage(e))
.build();
WebSockets.sendText(this.channel, JSON.toJSONString(resp));
return false; return false;
} }
} }
/**
* 发送消息
*
* @param type type
* @param ex ex
*/
private void sendMessage(TransferReceiverType type, Exception ex) {
TransferOperatorResponse resp = TransferOperatorResponse.builder()
.type(type.getType())
.success(ex == null)
.msg(this.getErrorMessage(ex))
.build();
WebSockets.sendText(this.channel, JSON.toJSONString(resp));
}
/** /**
* 获取错误信息 * 获取错误信息
* *

View File

@@ -11,8 +11,11 @@
:on-before-ok="handlerOk" :on-before-ok="handlerOk"
@cancel="handleClose"> @cancel="handleClose">
<div class="upload-container"> <div class="upload-container">
<div class="mb16"> <div class="parent-wrapper mb16">
上传至文件夹: {{ parentPath }} <span class="parent-label">上传至文件夹:</span>
<a-input class="parent-input"
v-model="parentPath"
placeholder="上传目录" />
</div> </div>
<a-space> <a-space>
<!-- 选择文件 --> <!-- 选择文件 -->
@@ -101,13 +104,17 @@
// 确定 // 确定
const handlerOk = () => { const handlerOk = () => {
if (!parentPath.value) {
Message.error('请输入上传目录');
return false;
}
if (!fileList.value.length) { if (!fileList.value.length) {
return true; return true;
} }
// 添加到上传列表 // 添加到上传列表
const files = fileList.value.map(s => { const files = fileList.value.map(s => {
return { return {
id: nextId(10), fileId: nextId(10),
type: TransferType.UPLOAD, type: TransferType.UPLOAD,
hostId: hostId.value, hostId: hostId.value,
name: s.file.webkitRelativePath || s.file.name, name: s.file.webkitRelativePath || s.file.name,
@@ -142,6 +149,20 @@
width: 100%; width: 100%;
} }
.parent-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.parent-label {
width: 98px;
}
.parent-input {
width: 386px;
}
}
.file-list-uploader { .file-list-uploader {
margin-top: 24px; margin-top: 24px;

View File

@@ -207,6 +207,7 @@
onMounted(async () => { onMounted(async () => {
// 创建终端处理器 // 创建终端处理器
session.value = await sessionManager.openSftp(props.tab, { session.value = await sessionManager.openSftp(props.tab, {
setLoading: setTableLoading,
connectCallback, connectCallback,
resolveList, resolveList,
resolveSftpMkdir: resolveFileAction, resolveSftpMkdir: resolveFileAction,

View File

@@ -2,7 +2,6 @@
<a-drawer v-model:visible="visible" <a-drawer v-model:visible="visible"
title="文件传输列表" title="文件传输列表"
:width="388" :width="388"
:mask-closable="false"
:unmount-on-close="false" :unmount-on-close="false"
:footer="false"> :footer="false">
<a-spin class="full" :loading="loading"> <a-spin class="full" :loading="loading">
@@ -42,7 +41,15 @@
</a-tooltip> </a-tooltip>
<!-- 传输进度 --> <!-- 传输进度 -->
<span class="transfer-progress"> <span class="transfer-progress">
{{ getFileSize(item.currentSize) }}/{{ getFileSize(item.totalSize) }} <!-- 当前大小 -->
<span v-if="item.status === TransferStatus.TRANSFERRING">{{ getFileSize(item.currentSize) }}</span>
<span class="mx4" v-if="item.status === TransferStatus.TRANSFERRING">/</span>
<!-- 总大小 -->
<span>{{ getFileSize(item.totalSize) }}</span>
<!-- 进度百分比 -->
<span class="ml8" v-if="item.status === TransferStatus.TRANSFERRING">
{{ (item.currentSize / item.totalSize * 100).toFixed(2) }}%
</span>
</span> </span>
<!-- 目标目录 --> <!-- 目标目录 -->
<a-tooltip v-if="item.parentPath" <a-tooltip v-if="item.parentPath"
@@ -71,14 +78,24 @@
</div> </div>
<!-- 右侧状态/操作--> <!-- 右侧状态/操作-->
<div class="transfer-item-right"> <div class="transfer-item-right">
<!-- 等待传输 --> <!-- 传输状态 -->
<icon-loading v-if="item.status === TransferStatus.WAITING" /> <div class="transfer-item-right-progress">
<!-- 传输进度 --> <!-- 等待传输 -->
<a-progress v-else <icon-loading v-if="item.status === TransferStatus.WAITING" />
type="circle" <!-- 传输进度 -->
size="mini" <a-progress v-else
:status="item.status" type="circle"
:percent="item.currentSize / item.totalSize" /> size="mini"
:status="item.status"
:percent="item.currentSize / item.totalSize" />
</div>
<!-- 传输操作 -->
<div class="transfer-item-right-actions">
<!-- 关闭 -->
<span class="close-icon" @click="removeTask(item.fileId)">
<icon-close />
</span>
</div>
</div> </div>
</div> </div>
</a-list-item> </a-list-item>
@@ -112,6 +129,11 @@
defineExpose({ open }); defineExpose({ open });
// 移除任务
const removeTask = (fileId: string) => {
transferManager.cancelTransfer(fileId);
};
// 关闭 // 关闭
const handleClose = () => { const handleClose = () => {
handlerClear(); handlerClear();
@@ -125,6 +147,7 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@icon-size: 20px;
@item-left-width: 42px; @item-left-width: 42px;
@item-right-width: 42px; @item-right-width: 42px;
@item-center-width: 388px - @item-left-width - @item-right-width; @item-center-width: 388px - @item-left-width - @item-right-width;
@@ -135,6 +158,16 @@
display: flex; display: flex;
align-items: center; align-items: center;
&:hover {
.transfer-item-right-progress {
display: none;
}
.transfer-item-right-actions {
display: flex;
}
}
&-left { &-left {
width: @item-left-width; width: @item-left-width;
display: flex; display: flex;
@@ -173,8 +206,30 @@
&-right { &-right {
width: @item-right-width; width: @item-right-width;
display: flex;
justify-content: center; &-progress {
display: flex;
justify-content: center;
}
&-actions {
display: none;
justify-content: center;
.close-icon {
width: @icon-size;
height: @icon-size;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background: var(--color-fill-2);
}
}
}
} }
} }

View File

@@ -56,6 +56,7 @@ export default class SftpSession implements ISftpSession {
// 创建文件夹 // 创建文件夹
mkdir(path: string) { mkdir(path: string) {
this.resolver.setLoading(true);
this.channel.send(InputProtocol.SFTP_MKDIR, { this.channel.send(InputProtocol.SFTP_MKDIR, {
sessionId: this.sessionId, sessionId: this.sessionId,
path path
@@ -64,6 +65,7 @@ export default class SftpSession implements ISftpSession {
// 创建文件 // 创建文件
touch(path: string) { touch(path: string) {
this.resolver.setLoading(true);
this.channel.send(InputProtocol.SFTP_TOUCH, { this.channel.send(InputProtocol.SFTP_TOUCH, {
sessionId: this.sessionId, sessionId: this.sessionId,
path path
@@ -72,6 +74,7 @@ export default class SftpSession implements ISftpSession {
// 移动文件 // 移动文件
move(path: string, target: string) { move(path: string, target: string) {
this.resolver.setLoading(true);
this.channel.send(InputProtocol.SFTP_MOVE, { this.channel.send(InputProtocol.SFTP_MOVE, {
sessionId: this.sessionId, sessionId: this.sessionId,
path, path,
@@ -85,6 +88,7 @@ export default class SftpSession implements ISftpSession {
title: '删除确认', title: '删除确认',
content: `确定要删除 ${path} 吗? 确定后将立即删除且无法恢复!`, content: `确定要删除 ${path} 吗? 确定后将立即删除且无法恢复!`,
onOk: () => { onOk: () => {
this.resolver.setLoading(true);
this.channel.send(InputProtocol.SFTP_REMOVE, { this.channel.send(InputProtocol.SFTP_REMOVE, {
sessionId: this.sessionId, sessionId: this.sessionId,
path: path.join('|') path: path.join('|')
@@ -95,6 +99,7 @@ export default class SftpSession implements ISftpSession {
// 修改权限 // 修改权限
chmod(path: string, mod: number) { chmod(path: string, mod: number) {
this.resolver.setLoading(true);
this.channel.send(InputProtocol.SFTP_CHMOD, { this.channel.send(InputProtocol.SFTP_CHMOD, {
sessionId: this.sessionId, sessionId: this.sessionId,
path, path,

View File

@@ -1,15 +1,12 @@
import type { ISftpTransferManager, ISftpTransferUploader, SftpTransferItem } from '../types/terminal.type'; import type { ISftpTransferManager, ISftpTransferUploader, SftpTransferItem } from '../types/terminal.type';
import { TransferOperatorResponse } from '../types/terminal.type'; import { TransferOperatorResponse } from '../types/terminal.type';
import { TransferOperatorType, TransferStatus, TransferType } from '../types/terminal.const'; import { TransferReceiverType, TransferStatus, TransferType } from '../types/terminal.const';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { getTerminalAccessToken } from '@/api/asset/host-terminal'; import { getTerminalAccessToken } from '@/api/asset/host-terminal';
import SftpTransferUploader from '@/views/host/terminal/handler/sftp-transfer-uploader'; import SftpTransferUploader from '@/views/host/terminal/handler/sftp-transfer-uploader';
export const wsBase = import.meta.env.VITE_WS_BASE_URL; export const wsBase = import.meta.env.VITE_WS_BASE_URL;
// todo 考虑一下单文件上传失败 (网络/文件被删除)
// todo 取消任务
// sftp 传输管理器实现 // sftp 传输管理器实现
export default class SftpTransferManager implements ISftpTransferManager { export default class SftpTransferManager implements ISftpTransferManager {
@@ -37,6 +34,23 @@ export default class SftpTransferManager implements ISftpTransferManager {
} }
} }
// 取消传输
cancelTransfer(fileId: string): void {
const index = this.transferList.findIndex(s => s.fileId === fileId);
if (index === -1) {
return;
}
const item = this.transferList[index];
if (item.status === TransferStatus.TRANSFERRING) {
// 传输中则中断传输
if (this.currentUploader) {
this.currentUploader.uploadAbort();
}
}
// 从列表中移除
this.transferList.splice(index, 1);
}
// 打开会话 // 打开会话
private async openClient() { private async openClient() {
this.run = true; this.run = true;
@@ -91,9 +105,12 @@ export default class SftpTransferManager implements ISftpTransferManager {
// 接收消息 // 接收消息
private async resolveMessage(message: MessageEvent) { private async resolveMessage(message: MessageEvent) {
const data = JSON.parse(message.data) as TransferOperatorResponse; const data = JSON.parse(message.data) as TransferOperatorResponse;
if (data.type === TransferOperatorType.PROCESSED) { if (data.type === TransferReceiverType.NEXT_BLOCK) {
// 接收处理完成 // 接收下一块上传数据
this.resolveProcessed(data); await this.resolveNextBlock();
} else if (data.type === TransferReceiverType.NEXT_TRANSFER) {
// 接收接收下一个传输任务处理完成
this.resolveNextTransfer(data);
} }
} }
@@ -110,35 +127,40 @@ export default class SftpTransferManager implements ISftpTransferManager {
// TODO // TODO
} }
// 接收处理完成回调 // 接收下一块上传数据
private resolveProcessed(data: TransferOperatorResponse) { private async resolveNextBlock() {
// 操作回调 // 只可能为上传并且成功
if (data.success) { if (!this.currentUploader) {
// 操作成功 return;
if (this.currentUploader) { }
if (this.currentUploader.hasNextBlock()) { if (this.currentUploader.hasNextBlock()
// 有下一个分片则上传 (上一个分片传输完成) && !this.currentUploader.abort
this.currentUploader.uploadNextBlock(); && !this.currentUploader.finish) {
} else { try {
// 有下一个分片则检查是否完成 // 有下一个分片则上传 (上一个分片传输完成)
if (this.currentUploader.finish) { await this.currentUploader.uploadNextBlock();
// 已完成 开始下一个传输任务 (发送 finish 后的回调) } catch (e) {
this.transferNextItem(); // 读取文件失败
} else { this.currentUploader.uploadError((e as Error).message);
// 未完成则发送完成 (最后一个分片传输完成但还未发送 finish 指令)
this.currentUploader.uploadFinish();
}
}
} }
} else { } else {
// 操作失败 // 没有下一个分片则发送完成
if (this.currentUploader) { this.currentUploader.uploadFinish();
// 上传失败
this.currentUploader.uploadError(data.msg);
}
// 开始下一个传输任务
this.transferNextItem();
} }
} }
// 接收下一个传输任务
private resolveNextTransfer(data: TransferOperatorResponse) {
if (this.currentItem) {
if (data.success) {
this.currentItem.status = TransferStatus.SUCCESS;
} else {
this.currentItem.status = TransferStatus.ERROR;
this.currentItem.errorMessage = data.msg || '上传失败';
}
}
// 开始下一个传输任务
this.transferNextItem();
}
} }

View File

@@ -7,6 +7,7 @@ export const BLOCK_SIZE = 1024 * 1024;
// sftp 上传器实现 // sftp 上传器实现
export default class SftpTransferUploader implements ISftpTransferUploader { export default class SftpTransferUploader implements ISftpTransferUploader {
public abort: boolean;
public finish: boolean; public finish: boolean;
private currentBlock: number; private currentBlock: number;
private totalBlock: number; private totalBlock: number;
@@ -15,6 +16,7 @@ export default class SftpTransferUploader implements ISftpTransferUploader {
private file: File; private file: File;
constructor(item: SftpTransferItem, client: WebSocket) { constructor(item: SftpTransferItem, client: WebSocket) {
this.abort = false;
this.finish = false; this.finish = false;
this.item = item; this.item = item;
this.client = client; this.client = client;
@@ -73,6 +75,16 @@ export default class SftpTransferUploader implements ISftpTransferUploader {
this.finish = true; this.finish = true;
this.item.status = TransferStatus.ERROR; this.item.status = TransferStatus.ERROR;
this.item.errorMessage = msg || '上传失败'; this.item.errorMessage = msg || '上传失败';
// 发送上传完成的信息
this.client?.send(JSON.stringify({
type: TransferOperatorType.UPLOAD_ERROR,
hostId: this.item.hostId
}));
}
// 上传中断
uploadAbort() {
this.abort = true;
} }
} }

View File

@@ -301,9 +301,15 @@ export const TransferType = {
// 传输操作类型 // 传输操作类型
export const TransferOperatorType = { export const TransferOperatorType = {
PROCESSED: 'processed', UPLOAD_START: 'uploadStart',
UPLOAD_START: 'upload_start', UPLOAD_FINISH: 'uploadFinish',
UPLOAD_FINISH: 'upload_finish' UPLOAD_ERROR: 'uploadError',
};
// 传输响应类型
export const TransferReceiverType = {
NEXT_BLOCK: 'nextBlock',
NEXT_TRANSFER: 'nextTransfer',
}; };
// 打开 sshSettingModal key // 打开 sshSettingModal key

View File

@@ -333,6 +333,8 @@ export interface ISftpSession extends ITerminalSession {
// sftp 会话接收器定义 // sftp 会话接收器定义
export interface ISftpSessionResolver { export interface ISftpSessionResolver {
// 设置加载状态
setLoading: (loading: boolean) => void;
// 连接后回调 // 连接后回调
connectCallback: () => void; connectCallback: () => void;
// 接受文件列表响应 // 接受文件列表响应
@@ -372,27 +374,33 @@ export interface ISftpTransferManager {
transferList: Array<SftpTransferItem>; transferList: Array<SftpTransferItem>;
// 添加传输 // 添加传输
addTransfer: (items: Array<SftpTransferItem>) => void; addTransfer: (items: Array<SftpTransferItem>) => void;
// 取消传输
cancelTransfer: (fileId: string) => void;
} }
// sftp 上传器定义 // sftp 上传器定义
export interface ISftpTransferUploader { export interface ISftpTransferUploader {
// 是否完成 // 是否完成
finish: boolean; finish: boolean;
// 是否中断
abort: boolean;
// 开始上传 // 开始上传
startUpload: () => void; startUpload: () => void;
// 是否有下一个分片 // 是否有下一个分片
hasNextBlock: () => boolean; hasNextBlock: () => boolean;
// 上传下一个分片 // 上传下一个分片
uploadNextBlock: () => void; uploadNextBlock: () => Promise<void>;
// 上传完成 // 上传完成
uploadFinish: () => void; uploadFinish: () => void;
// 上传失败 // 上传失败
uploadError: (msg: string | undefined) => void; uploadError: (msg: string | undefined) => void;
// 上传中断
uploadAbort: () => void;
} }
// sftp 上传文件项 // sftp 上传文件项
export interface SftpTransferItem { export interface SftpTransferItem {
id: string; fileId: string;
type: string; type: string;
hostId: number; hostId: number;
name: string; name: string;