diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/pom.xml b/orion-ops-module-asset/orion-ops-module-asset-service/pom.xml
index 4c261f3b..b68fcf62 100644
--- a/orion-ops-module-asset/orion-ops-module-asset-service/pom.xml
+++ b/orion-ops-module-asset/orion-ops-module-asset-service/pom.xml
@@ -41,7 +41,7 @@
orion-ops-spring-boot-starter-web
-
+
com.orion.ops
orion-ops-spring-boot-starter-websocket
diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandAnsiHandler.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandAnsiHandler.java
index 42f1fd2e..744bd2e5 100644
--- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandAnsiHandler.java
+++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/exec/command/handler/ExecCommandAnsiHandler.java
@@ -4,7 +4,6 @@ import com.orion.lang.support.timeout.TimeoutChecker;
import com.orion.lang.support.timeout.TimeoutEndpoint;
import com.orion.lang.utils.Booleans;
import com.orion.lang.utils.ansi.AnsiAppender;
-import com.orion.lang.utils.ansi.style.AnsiFont;
import com.orion.lang.utils.ansi.style.color.AnsiForeground;
import com.orion.lang.utils.time.Dates;
import com.orion.net.host.ssh.ExitCode;
@@ -31,16 +30,16 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
super.initLogOutputStream();
// 拼接启动日志
AnsiAppender appender = AnsiAppender.create()
- .append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "> 准备执行命令 ")
+ .append(AnsiForeground.BRIGHT_GREEN, "> 准备执行命令 ")
.append(Dates.current())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "执行记录: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "执行记录: ")
.append(execCommand.getLogId())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "执行描述: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "执行描述: ")
.append(execCommand.getDescription())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "执行用户: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "执行用户: ")
.append(execCommand.getUsername());
// 非系统用户执行添加 userId
if (Const.SYSTEM_USER_ID.equals(execCommand.getUserId())) {
@@ -53,35 +52,35 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
}
// 执行序列
if (execCommand.getExecSeq() != null) {
- appender.append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "执行序列: ")
+ appender.append(AnsiForeground.BRIGHT_BLUE, "执行序列: ")
.append('#')
.append(execCommand.getExecSeq())
.newLine();
}
- appender.append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "执行主机: ")
+ appender.append(AnsiForeground.BRIGHT_BLUE, "执行主机: ")
.append(execHostCommand.getHostName())
.append(" (")
.append(execHostCommand.getHostId())
.append(")")
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "主机地址: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "主机地址: ")
.append(execHostCommand.getHostAddress())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "超时时间: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "超时时间: ")
.append(execCommand.getTimeout())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "脚本执行: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "脚本执行: ")
.append(execCommand.getScriptExec())
.newLine()
.newLine()
- .append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "> 执行命令 ")
+ .append(AnsiForeground.BRIGHT_GREEN, "> 执行命令 ")
.newLine()
.append(execHostCommand.getCommand())
.newLine()
.newLine();
// 非脚本执行拼接开始执行日志
if (!Booleans.isTrue(execCommand.getScriptExec())) {
- appender.append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "> 开始执行命令 ")
+ appender.append(AnsiForeground.BRIGHT_GREEN, "> 开始执行命令 ")
.append(Dates.current())
.newLine();
}
@@ -94,10 +93,10 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
// 拼接上传日志
AnsiAppender startAppender = AnsiAppender.create()
.newLine()
- .append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "> 准备上传脚本 ")
+ .append(AnsiForeground.BRIGHT_GREEN, "> 准备上传脚本 ")
.append(Dates.current())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "文件路径: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "文件路径: ")
.append(execHostCommand.getScriptPath())
.newLine();
this.appendLog(startAppender);
@@ -105,18 +104,18 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
super.uploadScriptFile();
// 拼接完成日志
AnsiAppender finishAppender = AnsiAppender.create()
- .append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "< 脚本上传成功 ")
+ .append(AnsiForeground.BRIGHT_GREEN, "< 脚本上传成功 ")
.append(Dates.current())
.newLine()
.newLine()
- .append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "> 开始执行脚本 ")
+ .append(AnsiForeground.BRIGHT_GREEN, "> 开始执行脚本 ")
.append(Dates.current())
.newLine();
this.appendLog(finishAppender);
} catch (Exception e) {
// 拼接失败日志
AnsiAppender errorAppender = AnsiAppender.create()
- .append(AnsiForeground.RED.and(AnsiFont.BOLD), "< 脚本上传失败 ")
+ .append(AnsiForeground.BRIGHT_RED, "< 脚本上传失败 ")
.append(Dates.current())
.newLine();
this.appendLog(errorAppender);
@@ -133,37 +132,37 @@ public class ExecCommandAnsiHandler extends BaseExecCommandHandler {
.newLine();
if (this.status == ExecHostStatusEnum.INTERRUPTED) {
// 中断执行
- appender.append(AnsiForeground.YELLOW.and(AnsiFont.BOLD), "< 命令执行中断 ")
+ appender.append(AnsiForeground.BRIGHT_YELLOW, "< 命令执行中断 ")
.append(Dates.current())
.newLine();
} else if (this.status == ExecHostStatusEnum.FAILED) {
// 执行失败
- appender.append(AnsiForeground.RED.and(AnsiFont.BOLD), "< 命令执行失败 ")
+ appender.append(AnsiForeground.BRIGHT_RED, "< 命令执行失败 ")
.append(Dates.current())
.newLine()
- .append(AnsiForeground.RED.and(AnsiFont.BOLD), "错误原因: ")
+ .append(AnsiForeground.BRIGHT_RED, "错误原因: ")
.append(this.getErrorMessage(e))
.newLine();
} else if (this.status == ExecHostStatusEnum.TIMEOUT) {
// 更新执行超时
- appender.append(AnsiForeground.YELLOW.and(AnsiFont.BOLD), "< 命令执行超时 ")
+ appender.append(AnsiForeground.BRIGHT_YELLOW, "< 命令执行超时 ")
.append(Dates.current())
.newLine();
} else {
long ms = this.updateRecord.getFinishTime().getTime() - this.updateRecord.getStartTime().getTime();
Integer exitStatus = this.updateRecord.getExitStatus();
// 执行完成
- appender.append(AnsiForeground.GREEN.and(AnsiFont.BOLD), "< 命令执行完成 ")
+ appender.append(AnsiForeground.BRIGHT_GREEN, "< 命令执行完成 ")
.append(Dates.current())
.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "exit: ");
+ .append(AnsiForeground.BRIGHT_BLUE, "exit: ");
if (ExitCode.isSuccess(exitStatus)) {
- appender.append(AnsiForeground.GREEN.and(AnsiFont.BOLD), exitStatus);
+ appender.append(AnsiForeground.BRIGHT_GREEN, exitStatus);
} else {
- appender.append(AnsiForeground.RED.and(AnsiFont.BOLD), exitStatus);
+ appender.append(AnsiForeground.BRIGHT_RED, exitStatus);
}
appender.newLine()
- .append(AnsiForeground.BLUE.and(AnsiFont.BOLD), "used: ")
+ .append(AnsiForeground.BRIGHT_BLUE, "used: ")
.append(Dates.interval(ms, false, "d ", "h ", "m ", "s"))
.append(" (")
.append(ms)
diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorRequest.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorRequest.java
index c22e70e9..6312353f 100644
--- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorRequest.java
+++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorRequest.java
@@ -2,9 +2,9 @@ package com.orion.ops.module.asset.handler.host.transfer.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
-import lombok.experimental.SuperBuilder;
/**
* 文件操作请求 实体对象
@@ -14,7 +14,7 @@ import lombok.experimental.SuperBuilder;
* @since 2024/2/21 21:01
*/
@Data
-@SuperBuilder
+@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "FileOperatorRequest", description = "文件操作请求 实体对象")
diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorResponse.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorResponse.java
index b4463d0c..06bd3da4 100644
--- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorResponse.java
+++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/transfer/model/TransferOperatorResponse.java
@@ -2,9 +2,9 @@ package com.orion.ops.module.asset.handler.host.transfer.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
-import lombok.experimental.SuperBuilder;
/**
* 文件操作响应 实体对象
@@ -14,7 +14,7 @@ import lombok.experimental.SuperBuilder;
* @since 2024/2/21 22:38
*/
@Data
-@SuperBuilder
+@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "FileOperatorResponse", description = "文件操作响应 实体对象")
diff --git a/orion-ops-module-infra/orion-ops-module-infra-provider/src/main/java/com/orion/ops/module/infra/api/FileUploadApi.java b/orion-ops-module-infra/orion-ops-module-infra-provider/src/main/java/com/orion/ops/module/infra/api/FileUploadApi.java
new file mode 100644
index 00000000..e94f5838
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-provider/src/main/java/com/orion/ops/module/infra/api/FileUploadApi.java
@@ -0,0 +1,21 @@
+package com.orion.ops.module.infra.api;
+
+/**
+ * 文件上传 对外服务类
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 15:58
+ */
+public interface FileUploadApi {
+
+ /**
+ * 生成上传 uploadToken
+ *
+ * @param userId userId
+ * @param endpoint endpoint
+ * @return token
+ */
+ String createUploadToken(Long userId, String endpoint);
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/pom.xml b/orion-ops-module-infra/orion-ops-module-infra-service/pom.xml
index aacc5b2c..910fab80 100644
--- a/orion-ops-module-infra/orion-ops-module-infra-service/pom.xml
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/pom.xml
@@ -34,6 +34,12 @@
orion-ops-spring-boot-starter-web
+
+
+ com.orion.ops
+ orion-ops-spring-boot-starter-websocket
+
+
com.orion.ops
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/api/impl/FileUploadApiImpl.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/api/impl/FileUploadApiImpl.java
new file mode 100644
index 00000000..79a5178f
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/api/impl/FileUploadApiImpl.java
@@ -0,0 +1,29 @@
+package com.orion.ops.module.infra.api.impl;
+
+import com.orion.ops.module.infra.api.FileUploadApi;
+import com.orion.ops.module.infra.service.FileUploadService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 文件上传 对外服务实现类
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 19:04
+ */
+@Slf4j
+@Service
+public class FileUploadApiImpl implements FileUploadApi {
+
+ @Resource
+ private FileUploadService fileUploadService;
+
+ @Override
+ public String createUploadToken(Long userId, String endpoint) {
+ return fileUploadService.createUploadToken(userId, endpoint);
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/configuration/InfraWebSocketConfiguration.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/configuration/InfraWebSocketConfiguration.java
new file mode 100644
index 00000000..90d31998
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/configuration/InfraWebSocketConfiguration.java
@@ -0,0 +1,39 @@
+package com.orion.ops.module.infra.configuration;
+
+import com.orion.ops.module.infra.handler.upload.FileUploadMessageDispatcher;
+import com.orion.ops.module.infra.interceptor.FileUploadInterceptor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+import javax.annotation.Resource;
+
+/**
+ * 基建模块 websocket 配置
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/12/28 11:39
+ */
+@Configuration
+public class InfraWebSocketConfiguration implements WebSocketConfigurer {
+
+ @Value("${orion.websocket.prefix}")
+ private String prefix;
+
+ @Resource
+ private FileUploadInterceptor fileUploadInterceptor;
+
+ @Resource
+ private FileUploadMessageDispatcher fileUploadMessageDispatcher;
+
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ // 文件上传
+ registry.addHandler(fileUploadMessageDispatcher, prefix + "/file/upload/{uploadToken}")
+ .addInterceptors(fileUploadInterceptor)
+ .setAllowedOrigins("*");
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/define/cache/FileUploadCacheKeyDefine.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/define/cache/FileUploadCacheKeyDefine.java
new file mode 100644
index 00000000..5e64fba5
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/define/cache/FileUploadCacheKeyDefine.java
@@ -0,0 +1,27 @@
+package com.orion.ops.module.infra.define.cache;
+
+import com.orion.lang.define.cache.key.CacheKeyBuilder;
+import com.orion.lang.define.cache.key.CacheKeyDefine;
+import com.orion.lang.define.cache.key.struct.RedisCacheStruct;
+import com.orion.ops.module.infra.entity.dto.FileUploadTokenDTO;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 文明上传缓存 key
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/8/31 11:48
+ */
+public interface FileUploadCacheKeyDefine {
+
+ CacheKeyDefine FILE_UPLOAD = new CacheKeyBuilder()
+ .key("file:upload:{}")
+ .desc("文件上传信息 ${token}")
+ .type(FileUploadTokenDTO.class)
+ .struct(RedisCacheStruct.STRING)
+ .timeout(3, TimeUnit.MINUTES)
+ .build();
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/entity/dto/FileUploadTokenDTO.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/entity/dto/FileUploadTokenDTO.java
new file mode 100644
index 00000000..b81833d4
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/entity/dto/FileUploadTokenDTO.java
@@ -0,0 +1,33 @@
+package com.orion.ops.module.infra.entity.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 文件上传 token 缓存对象
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023-7-13 18:42
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(name = "FileUploadTokenDTO", description = "文件上传 token 缓存对象")
+public class FileUploadTokenDTO implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "userId")
+ private Long userId;
+
+ @Schema(description = "上传父目录")
+ private String endpoint;
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/FileUploadMessageDispatcher.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/FileUploadMessageDispatcher.java
new file mode 100644
index 00000000..26077f7a
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/FileUploadMessageDispatcher.java
@@ -0,0 +1,82 @@
+package com.orion.ops.module.infra.handler.upload;
+
+import com.alibaba.fastjson.JSON;
+import com.orion.lang.utils.io.Streams;
+import com.orion.ops.framework.common.constant.ExtraFieldConst;
+import com.orion.ops.framework.common.file.FileClient;
+import com.orion.ops.framework.websocket.core.utils.WebSockets;
+import com.orion.ops.module.infra.entity.dto.FileUploadTokenDTO;
+import com.orion.ops.module.infra.handler.upload.enums.FileUploadOperatorType;
+import com.orion.ops.module.infra.handler.upload.handler.FileUploadHandler;
+import com.orion.ops.module.infra.handler.upload.handler.IFileUploadHandler;
+import com.orion.ops.module.infra.handler.upload.model.FileUploadRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.AbstractWebSocketHandler;
+
+import javax.annotation.Resource;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 上传消息处理器
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/2/21 18:22
+ */
+@Slf4j
+@Component
+public class FileUploadMessageDispatcher extends AbstractWebSocketHandler {
+
+ private final ConcurrentHashMap handlers = new ConcurrentHashMap<>();
+
+ @Resource
+ private FileClient localFileClient;
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+ // 获取处理器
+ IFileUploadHandler handler = handlers.computeIfAbsent(session.getId(), s -> {
+ FileUploadTokenDTO info = WebSockets.getAttr(session, ExtraFieldConst.INFO);
+ return new FileUploadHandler(session, localFileClient, info.getEndpoint());
+ });
+ // 处理消息
+ FileUploadRequest request = JSON.parseObject(message.getPayload(), FileUploadRequest.class);
+ FileUploadOperatorType type = FileUploadOperatorType.of(request.getType());
+ if (FileUploadOperatorType.START.equals(type)) {
+ // 开始上传
+ handler.start(request.getFileId());
+ } else if (FileUploadOperatorType.FINISH.equals(type)) {
+ // 上传完成
+ handler.finish();
+ }
+ }
+
+ @Override
+ protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
+ handlers.get(session.getId()).write(message.getPayload().array());
+ }
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+ log.info("FileUploadMessageDispatcher-afterConnectionEstablished id: {}", session.getId());
+ }
+
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) {
+ log.error("FileUploadMessageDispatcher-handleTransportError id: {}", session.getId(), exception);
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ String id = session.getId();
+ log.info("FileUploadMessageDispatcher-afterConnectionClosed id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason());
+ // 关闭会话
+ Streams.close(handlers.remove(id));
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/enums/FileUploadOperatorType.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/enums/FileUploadOperatorType.java
new file mode 100644
index 00000000..f1545861
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/enums/FileUploadOperatorType.java
@@ -0,0 +1,43 @@
+package com.orion.ops.module.infra.handler.upload.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文件上传操作类型
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 18:01
+ */
+@Getter
+@AllArgsConstructor
+public enum FileUploadOperatorType {
+
+ /**
+ * 开始上传
+ */
+ START("start"),
+
+ /**
+ * 上传完成
+ */
+ FINISH("finish"),
+
+ ;
+
+ private final String type;
+
+ public static FileUploadOperatorType of(String type) {
+ if (type == null) {
+ return null;
+ }
+ for (FileUploadOperatorType value : values()) {
+ if (value.type.equals(type)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/enums/FileUploadReceiverType.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/enums/FileUploadReceiverType.java
new file mode 100644
index 00000000..2fd70202
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/enums/FileUploadReceiverType.java
@@ -0,0 +1,48 @@
+package com.orion.ops.module.infra.handler.upload.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 文件上传响应类型
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 18:01
+ */
+@Getter
+@AllArgsConstructor
+public enum FileUploadReceiverType {
+
+ /**
+ * 请求下一块数据
+ */
+ NEXT("next"),
+
+ /**
+ * 上传完成
+ */
+ FINISH("finish"),
+
+ /**
+ * 上传失败
+ */
+ ERROR("error"),
+
+ ;
+
+ private final String type;
+
+ public static FileUploadReceiverType of(String type) {
+ if (type == null) {
+ return null;
+ }
+ for (FileUploadReceiverType value : values()) {
+ if (value.type.equals(type)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/handler/FileUploadHandler.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/handler/FileUploadHandler.java
new file mode 100644
index 00000000..6d76a0e8
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/handler/FileUploadHandler.java
@@ -0,0 +1,127 @@
+package com.orion.ops.module.infra.handler.upload.handler;
+
+import com.alibaba.fastjson.JSON;
+import com.orion.lang.utils.io.Streams;
+import com.orion.ops.framework.common.constant.Const;
+import com.orion.ops.framework.common.file.FileClient;
+import com.orion.ops.framework.websocket.core.utils.WebSockets;
+import com.orion.ops.module.infra.handler.upload.enums.FileUploadReceiverType;
+import com.orion.ops.module.infra.handler.upload.model.FileUploadResponse;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * 文件上传处理器实现
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 15:49
+ */
+public class FileUploadHandler implements IFileUploadHandler {
+
+ private final WebSocketSession channel;
+
+ private final FileClient fileClient;
+
+ private final String endpoint;
+
+ private String fileId;
+
+ private String filePath;
+
+ private OutputStream outputStream;
+
+ private boolean closed;
+
+ public FileUploadHandler(WebSocketSession channel, FileClient fileClient, String endpoint) {
+ this.channel = channel;
+ this.fileClient = fileClient;
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ public void start(String fileId) {
+ // 释放资源
+ this.close();
+ // 获取返回路径
+ this.fileId = fileId;
+ this.filePath = fileClient.getReturnPath(endpoint + Const.SLASH + fileId);
+ try {
+ // 打开文件流
+ this.outputStream = fileClient.getContentOutputStream(filePath);
+ this.closed = false;
+ // 请求下一块数据
+ FileUploadResponse resp = FileUploadResponse.builder()
+ .type(FileUploadReceiverType.NEXT.getType())
+ .fileId(this.fileId)
+ .build();
+ this.send(resp);
+ } catch (Exception e) {
+ // 释放资源
+ this.close();
+ // 返回错误
+ FileUploadResponse resp = FileUploadResponse.builder()
+ .type(FileUploadReceiverType.ERROR.getType())
+ .fileId(this.fileId)
+ .build();
+ this.send(resp);
+ }
+ }
+
+ @Override
+ public void write(byte[] content) {
+ try {
+ // 写入内容
+ this.outputStream.write(content);
+ // 请求下一块数据
+ FileUploadResponse resp = FileUploadResponse.builder()
+ .type(FileUploadReceiverType.NEXT.getType())
+ .fileId(this.fileId)
+ .build();
+ this.send(resp);
+ } catch (IOException e) {
+ // 释放资源
+ this.close();
+ // 返回错误
+ FileUploadResponse resp = FileUploadResponse.builder()
+ .type(FileUploadReceiverType.ERROR.getType())
+ .fileId(this.fileId)
+ .build();
+ this.send(resp);
+ }
+ }
+
+ @Override
+ public void finish() {
+ // 释放资源
+ this.close();
+ // 返回上传路径
+ FileUploadResponse resp = FileUploadResponse.builder()
+ .type(FileUploadReceiverType.FINISH.getType())
+ .fileId(this.fileId)
+ .path(this.filePath)
+ .build();
+ this.send(resp);
+ }
+
+ @Override
+ public void close() {
+ if (closed) {
+ return;
+ }
+ this.closed = true;
+ Streams.close(outputStream);
+ }
+
+ /**
+ * 发送消息
+ *
+ * @param resp resp
+ */
+ private void send(FileUploadResponse resp) {
+ WebSockets.sendText(channel, JSON.toJSONString(resp));
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/handler/IFileUploadHandler.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/handler/IFileUploadHandler.java
new file mode 100644
index 00000000..3afe4aa6
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/handler/IFileUploadHandler.java
@@ -0,0 +1,33 @@
+package com.orion.ops.module.infra.handler.upload.handler;
+
+import com.orion.lang.able.SafeCloseable;
+
+/**
+ * 文件上传处理器
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 15:49
+ */
+public interface IFileUploadHandler extends SafeCloseable {
+
+ /**
+ * 开始上传
+ *
+ * @param fileId fileId
+ */
+ void start(String fileId);
+
+ /**
+ * 写入内容
+ *
+ * @param content content
+ */
+ void write(byte[] content);
+
+ /**
+ * 上传结束
+ */
+ void finish();
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/model/FileUploadRequest.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/model/FileUploadRequest.java
new file mode 100644
index 00000000..6b90f4be
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/model/FileUploadRequest.java
@@ -0,0 +1,29 @@
+package com.orion.ops.module.infra.handler.upload.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 文件上传请求 实体对象
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 18:12
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(name = "FileUploadRequest", description = "文件上传请求 实体对象")
+public class FileUploadRequest {
+
+ @Schema(description = "type")
+ private String type;
+
+ @Schema(description = "fileId")
+ private String fileId;
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/model/FileUploadResponse.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/model/FileUploadResponse.java
new file mode 100644
index 00000000..3bf61a7b
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/handler/upload/model/FileUploadResponse.java
@@ -0,0 +1,32 @@
+package com.orion.ops.module.infra.handler.upload.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 文件上传响应 实体对象
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 18:12
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(name = "FileUploadResponse", description = "文件上传响应 实体对象")
+public class FileUploadResponse {
+
+ @Schema(description = "type")
+ private String type;
+
+ @Schema(description = "fileId")
+ private String fileId;
+
+ @Schema(description = "路径")
+ private String path;
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/interceptor/FileUploadInterceptor.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/interceptor/FileUploadInterceptor.java
new file mode 100644
index 00000000..aa663e82
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/interceptor/FileUploadInterceptor.java
@@ -0,0 +1,51 @@
+package com.orion.ops.module.infra.interceptor;
+
+import com.orion.lang.utils.Urls;
+import com.orion.ops.framework.common.constant.ExtraFieldConst;
+import com.orion.ops.module.infra.entity.dto.FileUploadTokenDTO;
+import com.orion.ops.module.infra.service.FileUploadService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 文件上传拦截器
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2023/12/27 23:53
+ */
+@Slf4j
+@Component
+public class FileUploadInterceptor implements HandshakeInterceptor {
+
+ @Resource
+ private FileUploadService fileUploadService;
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
+ // 获取 uploadToken
+ String uploadToken = Urls.getUrlSource(request.getURI().getPath());
+ log.info("FileUploadInterceptor-beforeHandshake start uploadToken: {}", uploadToken);
+ // 检查 uploadToken
+ FileUploadTokenDTO info = fileUploadService.checkUploadToken(uploadToken);
+ if (info == null) {
+ log.error("FileUploadInterceptor-beforeHandshake absent uploadToken: {}", uploadToken);
+ return false;
+ }
+ // 设置参数
+ attributes.put(ExtraFieldConst.INFO, info);
+ return true;
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+ }
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/service/FileUploadService.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/service/FileUploadService.java
new file mode 100644
index 00000000..1d8aa1cf
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/service/FileUploadService.java
@@ -0,0 +1,31 @@
+package com.orion.ops.module.infra.service;
+
+import com.orion.ops.module.infra.entity.dto.FileUploadTokenDTO;
+
+/**
+ * 文件上传服务
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 15:58
+ */
+public interface FileUploadService {
+
+ /**
+ * 生成上传 uploadToken
+ *
+ * @param userId userId
+ * @param endpoint endpoint
+ * @return token
+ */
+ String createUploadToken(Long userId, String endpoint);
+
+ /**
+ * 检查 uploadToken
+ *
+ * @param token token
+ * @return info
+ */
+ FileUploadTokenDTO checkUploadToken(String token);
+
+}
diff --git a/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/service/impl/FileUploadServiceImpl.java b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/service/impl/FileUploadServiceImpl.java
new file mode 100644
index 00000000..0b98b909
--- /dev/null
+++ b/orion-ops-module-infra/orion-ops-module-infra-service/src/main/java/com/orion/ops/module/infra/service/impl/FileUploadServiceImpl.java
@@ -0,0 +1,47 @@
+package com.orion.ops.module.infra.service.impl;
+
+import com.orion.lang.id.UUIds;
+import com.orion.ops.framework.redis.core.utils.RedisStrings;
+import com.orion.ops.module.infra.define.cache.FileUploadCacheKeyDefine;
+import com.orion.ops.module.infra.entity.dto.FileUploadTokenDTO;
+import com.orion.ops.module.infra.service.FileUploadService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 文件上传服务
+ *
+ * @author Jiahang Li
+ * @version 1.0.0
+ * @since 2024/5/7 16:43
+ */
+@Slf4j
+@Service
+public class FileUploadServiceImpl implements FileUploadService {
+
+ @Override
+ public String createUploadToken(Long userId, String endpoint) {
+ String token = UUIds.random32();
+ String key = FileUploadCacheKeyDefine.FILE_UPLOAD.format(token);
+ // 设置缓存
+ FileUploadTokenDTO info = FileUploadTokenDTO.builder()
+ .userId(userId)
+ .endpoint(endpoint)
+ .build();
+ RedisStrings.setJson(key, FileUploadCacheKeyDefine.FILE_UPLOAD, info);
+ return token;
+ }
+
+ @Override
+ public FileUploadTokenDTO checkUploadToken(String token) {
+ String key = FileUploadCacheKeyDefine.FILE_UPLOAD.format(token);
+ // 查询缓存
+ FileUploadTokenDTO info = RedisStrings.getJson(key, FileUploadCacheKeyDefine.FILE_UPLOAD);
+ if (info != null) {
+ // 删除缓存
+ RedisStrings.delete(key);
+ }
+ return info;
+ }
+
+}
diff --git a/orion-ops-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue b/orion-ops-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue
index d4f78d94..e1bfaf2d 100644
--- a/orion-ops-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue
+++ b/orion-ops-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue
@@ -161,9 +161,11 @@
const forceOffline = async (record: HostConnectLogQueryResponse) => {
try {
setLoading(true);
+ // 下线
await hostForceOffline({ id: record.id });
- record.endTime = Date.now();
Message.success('已下线');
+ // 移除行
+ tableRenderData.value.splice(tableRenderData.value.findIndex(s => s.id === record.id), 1);
} catch (e) {
} finally {
setLoading(false);