API数据表更新
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package com.mini.capi.biz.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 前端控制器
|
||||
* </p>
|
||||
*
|
||||
* @author gaoxq
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/biz/sshServers")
|
||||
public class SshServersController {
|
||||
|
||||
}
|
||||
99
src/main/java/com/mini/capi/biz/domain/SshServers.java
Normal file
99
src/main/java/com/mini/capi/biz/domain/SshServers.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.mini.capi.biz.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
*
|
||||
* </p>
|
||||
*
|
||||
* @author gaoxq
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("biz_ssh_servers")
|
||||
public class SshServers implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 服务器名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 服务器地址
|
||||
*/
|
||||
@TableField("host")
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* SSH端口
|
||||
*/
|
||||
@TableField("port")
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码(建议加密存储)
|
||||
*/
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
@TableField("update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@TableField("f_tenant_id")
|
||||
private String fTenantId;
|
||||
|
||||
/**
|
||||
* 流程id
|
||||
*/
|
||||
@TableField("f_flow_id")
|
||||
private String fFlowId;
|
||||
|
||||
/**
|
||||
* 流程任务主键
|
||||
*/
|
||||
@TableField("f_flow_task_id")
|
||||
private String fFlowTaskId;
|
||||
|
||||
/**
|
||||
* 流程任务状态
|
||||
*/
|
||||
@TableField("f_flow_state")
|
||||
private Integer fFlowState;
|
||||
|
||||
|
||||
|
||||
public SshServers(String name, String host, Integer port, String username, String password){
|
||||
this.name = name;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
16
src/main/java/com/mini/capi/biz/mapper/SshServersMapper.java
Normal file
16
src/main/java/com/mini/capi/biz/mapper/SshServersMapper.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.mini.capi.biz.mapper;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author gaoxq
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
public interface SshServersMapper extends BaseMapper<SshServers> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.mini.capi.biz.service;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 服务类
|
||||
* </p>
|
||||
*
|
||||
* @author gaoxq
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
public interface SshServersService extends IService<SshServers> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mini.capi.biz.service.impl;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.mini.capi.biz.mapper.SshServersMapper;
|
||||
import com.mini.capi.biz.service.SshServersService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author gaoxq
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Service
|
||||
public class SshServersServiceImpl extends ServiceImpl<SshServersMapper, SshServers> implements SshServersService {
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public class demo {
|
||||
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"));
|
||||
})
|
||||
.strategyConfig(builder -> {
|
||||
builder.addInclude("biz_sync_tables_view")
|
||||
builder.addInclude("biz_ssh_servers")
|
||||
.addTablePrefix("biz_")
|
||||
.entityBuilder()
|
||||
.enableLombok()
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class hostController {
|
||||
|
||||
|
||||
/**
|
||||
* 主机登录
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/Sys/app/host")
|
||||
public String listPage() {
|
||||
return "views/ssh/index";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.mini.capi.webssh.config;
|
||||
|
||||
import com.mini.capi.webssh.websocket.SSHWebSocketHandler;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Resource
|
||||
private SSHWebSocketHandler sshWebSocketHandler;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(sshWebSocketHandler, "/ssh")
|
||||
.setAllowedOriginPatterns("*"); // 生产环境中应该限制域名
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.mini.capi.webssh.controller;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.mini.capi.biz.service.SshServersService;
|
||||
import com.mini.capi.webssh.service.FileTransferService;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
public class FileTransferController {
|
||||
|
||||
@Resource
|
||||
private FileTransferService fileTransferService;
|
||||
|
||||
@Resource
|
||||
private SshServersService serverService;
|
||||
|
||||
/**
|
||||
* 上传文件到服务器
|
||||
*/
|
||||
@PostMapping("/upload/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> uploadFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("remotePath") String remotePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "文件不能为空"));
|
||||
}
|
||||
|
||||
fileTransferService.uploadFile(serverOpt.get(), file, remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "文件上传成功",
|
||||
"filename", file.getOriginalFilename(),
|
||||
"size", file.getSize()
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "上传失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件
|
||||
*/
|
||||
@PostMapping("/upload-batch/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> uploadFiles(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("files") MultipartFile[] files,
|
||||
@RequestParam("remotePath") String remotePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
if (files == null || files.length == 0) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "请选择要上传的文件"));
|
||||
}
|
||||
|
||||
fileTransferService.uploadFiles(serverOpt.get(), files, remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "批量上传成功",
|
||||
"count", files.length
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "批量上传失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器下载文件
|
||||
*/
|
||||
@GetMapping("/download/{serverId}")
|
||||
public ResponseEntity<byte[]> downloadFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("remoteFilePath") String remoteFilePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
byte[] fileContent = fileTransferService.downloadFile(serverOpt.get(), remoteFilePath);
|
||||
|
||||
// 从路径中提取文件名
|
||||
String filename = remoteFilePath.substring(remoteFilePath.lastIndexOf('/') + 1);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(fileContent);
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出远程目录内容
|
||||
*/
|
||||
@GetMapping("/list/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> listDirectory(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("remotePath") String remotePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
List<FileTransferService.FileInfo> files =
|
||||
fileTransferService.listDirectory(serverOpt.get(), remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"files", files,
|
||||
"path", remotePath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "获取目录列表失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程目录
|
||||
*/
|
||||
@PostMapping("/mkdir/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> createDirectory(
|
||||
@PathVariable Long serverId,
|
||||
@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
String remotePath = request.get("remotePath");
|
||||
if (remotePath == null || remotePath.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "目录路径不能为空"));
|
||||
}
|
||||
|
||||
fileTransferService.createRemoteDirectory(serverOpt.get(), remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "目录创建成功",
|
||||
"path", remotePath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "创建目录失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除远程文件或目录
|
||||
*/
|
||||
@DeleteMapping("/delete/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> deleteFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("remotePath") String remotePath,
|
||||
@RequestParam(value = "isDirectory", defaultValue = "false") boolean isDirectory) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
fileTransferService.deleteRemoteFile(serverOpt.get(), remotePath, isDirectory);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", (isDirectory ? "目录" : "文件") + "删除成功",
|
||||
"path", remotePath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "删除失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名远程文件
|
||||
*/
|
||||
@PostMapping("/rename/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> renameFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
String oldPath = request.get("oldPath");
|
||||
String newPath = request.get("newPath");
|
||||
|
||||
if (oldPath == null || newPath == null || oldPath.trim().isEmpty() || newPath.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "路径不能为空"));
|
||||
}
|
||||
|
||||
fileTransferService.renameRemoteFile(serverOpt.get(), oldPath, newPath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "重命名成功",
|
||||
"oldPath", oldPath,
|
||||
"newPath", newPath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "重命名失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.mini.capi.webssh.controller;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.mini.capi.biz.service.SshServersService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/servers")
|
||||
public class ServerController {
|
||||
|
||||
@Resource
|
||||
private SshServersService serverService;
|
||||
|
||||
/**
|
||||
* 获取服务器列表
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<SshServers>> getServers() {
|
||||
List<SshServers> servers = serverService.list();
|
||||
return ResponseEntity.ok(servers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个服务器配置
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<SshServers> getServer(@PathVariable Long id) {
|
||||
try {
|
||||
Optional<SshServers> server = serverService.getOptById(id);
|
||||
if (server.isPresent()) {
|
||||
return ResponseEntity.ok(server.get());
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加服务器
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> addServer(@RequestBody SshServers server) {
|
||||
try {
|
||||
// 验证必要参数
|
||||
if (server.getHost() == null || server.getHost().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "服务器地址不能为空"));
|
||||
}
|
||||
if (server.getUsername() == null || server.getUsername().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "用户名不能为空"));
|
||||
}
|
||||
if (server.getPassword() == null || server.getPassword().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "密码不能为空"));
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if (server.getPort() == null) {
|
||||
server.setPort(22);
|
||||
}
|
||||
if (server.getName() == null || server.getName().trim().isEmpty()) {
|
||||
server.setName(server.getUsername() + "@" + server.getHost());
|
||||
}
|
||||
|
||||
serverService.save(server);
|
||||
return ResponseEntity.ok(Map.of("success", true));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务器
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteServer(@PathVariable Long id) {
|
||||
try {
|
||||
serverService.removeById(id);
|
||||
return ResponseEntity.ok(Map.of("success", true));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试服务器连接
|
||||
*/
|
||||
@PostMapping("/test")
|
||||
public ResponseEntity<Map<String, Object>> testConnection(@RequestBody SshServers server) {
|
||||
try {
|
||||
// 验证必要参数
|
||||
if (server.getHost() == null || server.getHost().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "服务器地址不能为空"));
|
||||
}
|
||||
if (server.getUsername() == null || server.getUsername().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "用户名不能为空"));
|
||||
}
|
||||
if (server.getPassword() == null || server.getPassword().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "密码不能为空"));
|
||||
}
|
||||
|
||||
// 设置默认端口
|
||||
int port = server.getPort() != null ? server.getPort() : 22;
|
||||
|
||||
// 简单的连接测试
|
||||
JSch jsch = new JSch();
|
||||
Session session = jsch.getSession(server.getUsername(), server.getHost(), port);
|
||||
session.setPassword(server.getPassword());
|
||||
session.setConfig("StrictHostKeyChecking", "no");
|
||||
session.connect(5000); // 5秒超时
|
||||
session.disconnect();
|
||||
return ResponseEntity.ok(Map.of("success", true, "message", "连接测试成功"));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(Map.of("success", false, "message", "连接测试失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.mini.capi.webssh.service;
|
||||
|
||||
import com.jcraft.jsch.*;
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Vector;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class FileTransferService {
|
||||
|
||||
/**
|
||||
* 上传文件到远程服务器
|
||||
*/
|
||||
public void uploadFile(SshServers server, MultipartFile file, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
// 确保远程目录存在
|
||||
createRemoteDirectory(sftpChannel, remotePath);
|
||||
|
||||
// 上传文件
|
||||
String remoteFilePath = remotePath + "/" + file.getOriginalFilename();
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
sftpChannel.put(inputStream, remoteFilePath);
|
||||
}
|
||||
|
||||
log.info("文件上传成功: {} -> {}", file.getOriginalFilename(), remoteFilePath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程服务器下载文件
|
||||
*/
|
||||
public byte[] downloadFile(SshServers server, String remoteFilePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
InputStream inputStream = sftpChannel.get(remoteFilePath)) {
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
log.info("文件下载成功: {}", remoteFilePath);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出远程目录内容
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<FileInfo> listDirectory(SshServers server, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
Vector<ChannelSftp.LsEntry> entries = sftpChannel.ls(remotePath);
|
||||
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
String filename = entry.getFilename();
|
||||
if (!filename.equals(".") && !filename.equals("..")) {
|
||||
SftpATTRS attrs = entry.getAttrs();
|
||||
files.add(new FileInfo(
|
||||
filename,
|
||||
attrs.isDir(),
|
||||
attrs.getSize(),
|
||||
attrs.getMTime() * 1000L, // Convert to milliseconds
|
||||
getPermissionString(attrs.getPermissions())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
log.info("目录列表获取成功: {}, 文件数: {}", remotePath, files.size());
|
||||
return files;
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程目录
|
||||
*/
|
||||
public void createRemoteDirectory(SshServers server, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
createRemoteDirectory(sftpChannel, remotePath);
|
||||
log.info("远程目录创建成功: {}", remotePath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除远程文件或目录
|
||||
*/
|
||||
public void deleteRemoteFile(SshServers server, String remotePath, boolean isDirectory) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
if (isDirectory) {
|
||||
sftpChannel.rmdir(remotePath);
|
||||
} else {
|
||||
sftpChannel.rm(remotePath);
|
||||
}
|
||||
|
||||
log.info("远程文件删除成功: {}", remotePath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名远程文件
|
||||
*/
|
||||
public void renameRemoteFile(SshServers server, String oldPath, String newPath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
sftpChannel.rename(oldPath, newPath);
|
||||
log.info("文件重命名成功: {} -> {}", oldPath, newPath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件
|
||||
*/
|
||||
public void uploadFiles(SshServers server, MultipartFile[] files, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
// 确保远程目录存在
|
||||
createRemoteDirectory(sftpChannel, remotePath);
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (!file.isEmpty()) {
|
||||
String remoteFilePath = remotePath + "/" + file.getOriginalFilename();
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
sftpChannel.put(inputStream, remoteFilePath);
|
||||
log.info("文件上传成功: {}", file.getOriginalFilename());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量上传完成,共上传 {} 个文件", files.length);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
// 私有辅助方法
|
||||
|
||||
private Session createSession(SshServers server) throws JSchException {
|
||||
JSch jsch = new JSch();
|
||||
Session session = jsch.getSession(server.getUsername(), server.getHost(), server.getPort());
|
||||
session.setPassword(server.getPassword());
|
||||
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
config.put("PreferredAuthentications", "password");
|
||||
session.setConfig(config);
|
||||
session.connect(10000); // 10秒超时
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private void createRemoteDirectory(ChannelSftp sftpChannel, String remotePath) {
|
||||
try {
|
||||
String[] pathParts = remotePath.split("/");
|
||||
String currentPath = "";
|
||||
|
||||
for (String part : pathParts) {
|
||||
if (!part.isEmpty()) {
|
||||
currentPath += "/" + part;
|
||||
try {
|
||||
sftpChannel.mkdir(currentPath);
|
||||
} catch (SftpException e) {
|
||||
log.error(e.getMessage(),e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("创建远程目录失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void closeConnections(ChannelSftp sftpChannel, Session session) {
|
||||
if (sftpChannel != null && sftpChannel.isConnected()) {
|
||||
sftpChannel.disconnect();
|
||||
}
|
||||
if (session != null && session.isConnected()) {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private String getPermissionString(int permissions) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// Owner permissions
|
||||
sb.append((permissions & 0400) != 0 ? 'r' : '-');
|
||||
sb.append((permissions & 0200) != 0 ? 'w' : '-');
|
||||
sb.append((permissions & 0100) != 0 ? 'x' : '-');
|
||||
|
||||
// Group permissions
|
||||
sb.append((permissions & 0040) != 0 ? 'r' : '-');
|
||||
sb.append((permissions & 0020) != 0 ? 'w' : '-');
|
||||
sb.append((permissions & 0010) != 0 ? 'x' : '-');
|
||||
|
||||
// Others permissions
|
||||
sb.append((permissions & 0004) != 0 ? 'r' : '-');
|
||||
sb.append((permissions & 0002) != 0 ? 'w' : '-');
|
||||
sb.append((permissions & 0001) != 0 ? 'x' : '-');
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// 文件信息内部类
|
||||
public static class FileInfo {
|
||||
private String name;
|
||||
private boolean isDirectory;
|
||||
private long size;
|
||||
private long lastModified;
|
||||
private String permissions;
|
||||
|
||||
public FileInfo(String name, boolean isDirectory, long size, long lastModified, String permissions) {
|
||||
this.name = name;
|
||||
this.isDirectory = isDirectory;
|
||||
this.size = size;
|
||||
this.lastModified = lastModified;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public boolean isDirectory() { return isDirectory; }
|
||||
public long getSize() { return size; }
|
||||
public long getLastModified() { return lastModified; }
|
||||
public String getPermissions() { return permissions; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.mini.capi.webssh.service;
|
||||
|
||||
import com.jcraft.jsch.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SSHConnectionManager {
|
||||
|
||||
private final Map<String, Session> connections = new ConcurrentHashMap<>();
|
||||
private final Map<String, ChannelShell> channels = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 建立SSH连接
|
||||
*/
|
||||
public String createConnection(String host, int port, String username, String password) {
|
||||
try {
|
||||
JSch jsch = new JSch();
|
||||
Session session = jsch.getSession(username, host, port);
|
||||
|
||||
// 配置连接参数
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
config.put("PreferredAuthentications", "password");
|
||||
session.setConfig(config);
|
||||
session.setPassword(password);
|
||||
|
||||
// 建立连接
|
||||
session.connect(30000); // 30秒超时
|
||||
|
||||
// 创建Shell通道
|
||||
ChannelShell channel = (ChannelShell) session.openChannel("shell");
|
||||
channel.setPty(true);
|
||||
channel.setPtyType("xterm", 80, 24, 640, 480);
|
||||
|
||||
// 生成连接ID
|
||||
String connectionId = UUID.randomUUID().toString();
|
||||
|
||||
// 保存连接和通道
|
||||
connections.put(connectionId, session);
|
||||
channels.put(connectionId, channel);
|
||||
|
||||
log.info("SSH连接建立成功: {}@{}:{}", username, host, port);
|
||||
return connectionId;
|
||||
|
||||
} catch (JSchException e) {
|
||||
log.error("SSH连接失败: {}", e.getMessage());
|
||||
throw new RuntimeException("SSH连接失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SSH通道
|
||||
*/
|
||||
public ChannelShell getChannel(String connectionId) {
|
||||
return channels.get(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SSH会话
|
||||
*/
|
||||
public Session getSession(String connectionId) {
|
||||
return connections.get(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭SSH连接
|
||||
*/
|
||||
public void closeConnection(String connectionId) {
|
||||
ChannelShell channel = channels.remove(connectionId);
|
||||
if (channel != null && channel.isConnected()) {
|
||||
channel.disconnect();
|
||||
}
|
||||
|
||||
Session session = connections.remove(connectionId);
|
||||
if (session != null && session.isConnected()) {
|
||||
session.disconnect();
|
||||
}
|
||||
|
||||
log.info("SSH连接已关闭: {}", connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
public boolean isConnected(String connectionId) {
|
||||
Session session = connections.get(connectionId);
|
||||
return session != null && session.isConnected();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.mini.capi.webssh.websocket;
|
||||
|
||||
import com.mini.capi.webssh.service.SSHConnectionManager;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jcraft.jsch.ChannelShell;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SSHWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private SSHConnectionManager connectionManager;
|
||||
|
||||
private final Map<WebSocketSession, String> sessionConnections = new ConcurrentHashMap<>();
|
||||
private final Map<WebSocketSession, String> sessionUsers = new ConcurrentHashMap<>();
|
||||
|
||||
// 为每个WebSocket会话添加同步锁
|
||||
private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
log.info("WebSocket连接建立: {}", session.getId());
|
||||
// 为每个会话创建同步锁
|
||||
sessionLocks.put(session, new Object());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
try {
|
||||
String payload = message.getPayload();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode jsonNode = mapper.readTree(payload);
|
||||
|
||||
String type = jsonNode.get("type").asText();
|
||||
|
||||
switch (type) {
|
||||
case "connect":
|
||||
handleConnect(session, jsonNode);
|
||||
break;
|
||||
case "command":
|
||||
handleCommand(session, jsonNode);
|
||||
break;
|
||||
case "resize":
|
||||
handleResize(session, jsonNode);
|
||||
break;
|
||||
case "disconnect":
|
||||
handleDisconnect(session);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的消息类型: {}", type);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket消息失败", e);
|
||||
sendError(session, "处理消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SSH连接请求
|
||||
*/
|
||||
private void handleConnect(WebSocketSession session, JsonNode jsonNode) {
|
||||
try {
|
||||
String host = jsonNode.get("host").asText();
|
||||
int port = jsonNode.get("port").asInt(22);
|
||||
String username = jsonNode.get("username").asText();
|
||||
String password = jsonNode.get("password").asText();
|
||||
boolean enableCollaboration = jsonNode.has("enableCollaboration") &&
|
||||
jsonNode.get("enableCollaboration").asBoolean();
|
||||
|
||||
// 存储用户信息
|
||||
sessionUsers.put(session, username);
|
||||
|
||||
// 建立SSH连接
|
||||
String connectionId = connectionManager.createConnection(host, port, username, password);
|
||||
sessionConnections.put(session, connectionId);
|
||||
|
||||
// 启动SSH通道
|
||||
ChannelShell channel = connectionManager.getChannel(connectionId);
|
||||
startSSHChannel(session, channel);
|
||||
|
||||
// 发送连接成功消息
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("type", "connected");
|
||||
response.put("message", "SSH连接建立成功");
|
||||
sendMessage(session, response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("建立SSH连接失败", e);
|
||||
sendError(session, "连接失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理命令执行请求
|
||||
*/
|
||||
private void handleCommand(WebSocketSession session, JsonNode jsonNode) {
|
||||
String connectionId = sessionConnections.get(session);
|
||||
if (connectionId == null) {
|
||||
sendError(session, "SSH连接未建立");
|
||||
return;
|
||||
}
|
||||
|
||||
String command = jsonNode.get("command").asText();
|
||||
ChannelShell channel = connectionManager.getChannel(connectionId);
|
||||
String username = sessionUsers.get(session);
|
||||
|
||||
if (channel != null && channel.isConnected()) {
|
||||
try {
|
||||
// 发送命令到SSH通道
|
||||
OutputStream out = channel.getOutputStream();
|
||||
out.write(command.getBytes());
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
log.error("发送SSH命令失败", e);
|
||||
sendError(session, "命令执行失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动SSH通道并处理输出
|
||||
*/
|
||||
private void startSSHChannel(WebSocketSession session, ChannelShell channel) {
|
||||
try {
|
||||
// 连接通道
|
||||
channel.connect();
|
||||
|
||||
// 处理SSH输出
|
||||
InputStream in = channel.getInputStream();
|
||||
|
||||
// 在单独的线程中读取SSH输出
|
||||
new Thread(() -> {
|
||||
byte[] buffer = new byte[4096];
|
||||
try {
|
||||
while (channel.isConnected() && session.isOpen()) {
|
||||
if (in.available() > 0) {
|
||||
int len = in.read(buffer);
|
||||
if (len > 0) {
|
||||
String output = new String(buffer, 0, len, "UTF-8");
|
||||
|
||||
// 发送给当前会话
|
||||
sendMessage(session, Map.of(
|
||||
"type", "output",
|
||||
"data", output
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// 没有数据时短暂休眠,避免CPU占用过高
|
||||
Thread.sleep(10);
|
||||
}
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
log.warn("SSH输出读取中断: {}", e.getMessage());
|
||||
}
|
||||
}, "SSH-Output-Reader-" + session.getId()).start();
|
||||
|
||||
} catch (JSchException | IOException e) {
|
||||
log.error("启动SSH通道失败", e);
|
||||
sendError(session, "通道启动失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理终端大小调整
|
||||
*/
|
||||
private void handleResize(WebSocketSession session, JsonNode jsonNode) {
|
||||
String connectionId = sessionConnections.get(session);
|
||||
if (connectionId != null) {
|
||||
ChannelShell channel = connectionManager.getChannel(connectionId);
|
||||
if (channel != null) {
|
||||
try {
|
||||
int cols = jsonNode.get("cols").asInt();
|
||||
int rows = jsonNode.get("rows").asInt();
|
||||
channel.setPtySize(cols, rows, cols * 8, rows * 16);
|
||||
} catch (Exception e) {
|
||||
log.warn("调整终端大小失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断开连接
|
||||
*/
|
||||
private void handleDisconnect(WebSocketSession session) {
|
||||
String connectionId = sessionConnections.remove(session);
|
||||
String username = sessionUsers.remove(session);
|
||||
|
||||
if (connectionId != null) {
|
||||
connectionManager.closeConnection(connectionId);
|
||||
}
|
||||
// 清理锁资源
|
||||
sessionLocks.remove(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
handleDisconnect(session);
|
||||
log.info("WebSocket连接关闭: {}", session.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到WebSocket客户端(线程安全)
|
||||
*/
|
||||
private void sendMessage(WebSocketSession session, Object message) {
|
||||
Object lock = sessionLocks.get(session);
|
||||
if (lock == null) return;
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = mapper.writeValueAsString(message);
|
||||
session.sendMessage(new TextMessage(json));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息
|
||||
*/
|
||||
private void sendError(WebSocketSession session, String error) {
|
||||
sendMessage(session, Map.of(
|
||||
"type", "error",
|
||||
"message", error
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取用户信息
|
||||
*/
|
||||
private String getUserFromSession(WebSocketSession session) {
|
||||
// 简化实现,实际应用中可以从session中获取认证用户信息
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取主机信息
|
||||
*/
|
||||
private String getHostFromSession(WebSocketSession session) {
|
||||
// 简化实现,实际应用中可以保存连接信息
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,30 @@
|
||||
spring.application.name=cApi
|
||||
server.port=31001
|
||||
server.servlet.context-path=/cApi
|
||||
|
||||
server.compression.enabled=true
|
||||
server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
|
||||
server.tomcat.max-connections=200
|
||||
server.tomcat.threads.max=100
|
||||
server.tomcat.threads.min-spare=10
|
||||
spring.datasource.url=jdbc:mysql://192.168.31.189:33069/work?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
|
||||
spring.datasource.username=dream
|
||||
spring.datasource.password=info_dream
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
# ===============================
|
||||
# Logging
|
||||
# ===============================
|
||||
logging.level.root=INFO
|
||||
logging.level.com.example.webssh=DEBUG
|
||||
logging.file.name=logs/webssh.log
|
||||
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
|
||||
# ===============================
|
||||
# Custom WebSSH
|
||||
# ===============================
|
||||
webssh.ssh.connection-timeout=30000
|
||||
webssh.ssh.session-timeout=1800000
|
||||
webssh.ssh.max-connections-per-user=10
|
||||
webssh.file.upload-max-size=100MB
|
||||
webssh.file.temp-dir=/ogsapp/temp/webssh-uploads
|
||||
webssh.collaboration.enabled=true
|
||||
webssh.collaboration.max-participants=10
|
||||
webssh.collaboration.session-timeout=3600000
|
||||
26
src/main/resources/mapper/SshServersMapper.xml
Normal file
26
src/main/resources/mapper/SshServersMapper.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.mini.capi.biz.mapper.SshServersMapper">
|
||||
|
||||
<!-- 通用查询映射结果 -->
|
||||
<resultMap id="BaseResultMap" type="com.mini.capi.biz.domain.SshServers">
|
||||
<id column="id" property="id" />
|
||||
<result column="create_time" property="createTime" />
|
||||
<result column="name" property="name" />
|
||||
<result column="host" property="host" />
|
||||
<result column="port" property="port" />
|
||||
<result column="username" property="username" />
|
||||
<result column="password" property="password" />
|
||||
<result column="update_time" property="updateTime" />
|
||||
<result column="f_tenant_id" property="fTenantId" />
|
||||
<result column="f_flow_id" property="fFlowId" />
|
||||
<result column="f_flow_task_id" property="fFlowTaskId" />
|
||||
<result column="f_flow_state" property="fFlowState" />
|
||||
</resultMap>
|
||||
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
create_time, id, name, host, port, username, password, update_time, f_tenant_id, f_flow_id, f_flow_task_id, f_flow_state
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
980
src/main/resources/static/assets/js/webssh-multisession.js
Normal file
980
src/main/resources/static/assets/js/webssh-multisession.js
Normal file
@@ -0,0 +1,980 @@
|
||||
/**
|
||||
* Web SSH 多会话客户端
|
||||
* 支持多个SSH连接和会话管理
|
||||
*/
|
||||
|
||||
class MultiSessionWebSSHClient {
|
||||
constructor() {
|
||||
this.sessions = new Map(); // 存储所有会话
|
||||
this.activeSessionId = null; // 当前激活的会话ID
|
||||
this.nextSessionId = 1; // 下一个会话ID
|
||||
this.savedServers = []; // 缓存保存的服务器列表
|
||||
|
||||
this.initializeUI();
|
||||
this.loadSavedServers();
|
||||
}
|
||||
|
||||
initializeUI() {
|
||||
// 初始化时隐藏终端容器
|
||||
document.getElementById('terminalContainer').classList.add('hidden');
|
||||
}
|
||||
|
||||
// ========== 会话管理 ==========
|
||||
createSession(host, port, username, password) {
|
||||
const sessionId = `session_${this.nextSessionId++}`;
|
||||
const serverName = `${username}@${host}:${port}`;
|
||||
|
||||
const session = {
|
||||
id: sessionId,
|
||||
host: host,
|
||||
port: port,
|
||||
username: username,
|
||||
password: password,
|
||||
name: serverName,
|
||||
terminal: null,
|
||||
websocket: null,
|
||||
fitAddon: null,
|
||||
connected: false,
|
||||
serverId: null
|
||||
};
|
||||
|
||||
// 创建终端实例
|
||||
session.terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#ffffff',
|
||||
selection: '#ffffff40'
|
||||
},
|
||||
rows: 25,
|
||||
cols: 100
|
||||
});
|
||||
|
||||
session.fitAddon = new FitAddon.FitAddon();
|
||||
session.terminal.loadAddon(session.fitAddon);
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
this.createTabForSession(session);
|
||||
this.createTerminalForSession(session);
|
||||
this.switchToSession(sessionId);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
createTabForSession(session) {
|
||||
const tabsContainer = document.getElementById('terminalTabs');
|
||||
|
||||
const tab = document.createElement('div');
|
||||
tab.className = 'terminal-tab';
|
||||
tab.id = `tab_${session.id}`;
|
||||
tab.onclick = () => this.switchToSession(session.id);
|
||||
|
||||
tab.innerHTML = `
|
||||
<div class="tab-status disconnected"></div>
|
||||
<div class="tab-title" title="${session.name}">${session.name}</div>
|
||||
<div class="tab-actions">
|
||||
<button class="tab-btn" onclick="event.stopPropagation(); sshClient.duplicateSession('${session.id}')" title="复制会话">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="tab-btn" onclick="event.stopPropagation(); sshClient.closeSession('${session.id}')" title="关闭会话">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tabsContainer.appendChild(tab);
|
||||
}
|
||||
|
||||
createTerminalForSession(session) {
|
||||
const contentContainer = document.getElementById('terminalContent');
|
||||
|
||||
const sessionDiv = document.createElement('div');
|
||||
sessionDiv.className = 'terminal-session';
|
||||
sessionDiv.id = `session_${session.id}`;
|
||||
|
||||
sessionDiv.innerHTML = `
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-info">
|
||||
<span class="connection-status" id="status_${session.id}">
|
||||
🔴 未连接
|
||||
</span>
|
||||
</div>
|
||||
<div class="terminal-actions">
|
||||
<button class="terminal-btn" onclick="switchPage('files')">
|
||||
<i class="fas fa-folder"></i> 文件管理
|
||||
</button>
|
||||
<button class="terminal-btn" onclick="sshClient.disconnectSession('${session.id}')">
|
||||
<i class="fas fa-times"></i> 断开连接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-wrapper">
|
||||
<div id="terminal_${session.id}"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
contentContainer.appendChild(sessionDiv);
|
||||
|
||||
// 初始化终端
|
||||
session.terminal.open(document.getElementById(`terminal_${session.id}`));
|
||||
session.fitAddon.fit();
|
||||
}
|
||||
|
||||
switchToSession(sessionId) {
|
||||
// 更新标签状态
|
||||
document.querySelectorAll('.terminal-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`tab_${sessionId}`).classList.add('active');
|
||||
|
||||
// 更新内容显示
|
||||
document.querySelectorAll('.terminal-session').forEach(session => {
|
||||
session.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`session_${sessionId}`).classList.add('active');
|
||||
|
||||
this.activeSessionId = sessionId;
|
||||
|
||||
// 调整终端大小
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session && session.fitAddon) {
|
||||
setTimeout(() => session.fitAddon.fit(), 100);
|
||||
}
|
||||
|
||||
// 显示终端容器
|
||||
document.getElementById('terminalContainer').classList.remove('hidden');
|
||||
|
||||
this.updateStatusBar();
|
||||
}
|
||||
|
||||
updateStatusBar() {
|
||||
const session = this.sessions.get(this.activeSessionId);
|
||||
if (session && session.terminal) {
|
||||
const size = session.terminal.buffer.active;
|
||||
document.getElementById('terminalStats').textContent =
|
||||
`行: ${size.baseY + size.cursorY + 1}, 列: ${size.cursorX + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
closeSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (this.sessions.size === 1) {
|
||||
// 如果是最后一个会话,隐藏终端容器
|
||||
document.getElementById('terminalContainer').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
if (session.websocket) {
|
||||
session.websocket.close();
|
||||
}
|
||||
|
||||
// 清理DOM元素
|
||||
const tab = document.getElementById(`tab_${sessionId}`);
|
||||
const sessionDiv = document.getElementById(`session_${sessionId}`);
|
||||
if (tab) tab.remove();
|
||||
if (sessionDiv) sessionDiv.remove();
|
||||
|
||||
// 从sessions中删除
|
||||
this.sessions.delete(sessionId);
|
||||
|
||||
// 如果关闭的是当前激活会话,切换到其他会话
|
||||
if (sessionId === this.activeSessionId) {
|
||||
const remainingSessions = Array.from(this.sessions.keys());
|
||||
if (remainingSessions.length > 0) {
|
||||
this.switchToSession(remainingSessions[0]);
|
||||
} else {
|
||||
this.activeSessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.showAlert('会话已关闭', 'info');
|
||||
}
|
||||
|
||||
duplicateSession(sessionId) {
|
||||
const originalSession = this.sessions.get(sessionId);
|
||||
if (!originalSession) return;
|
||||
|
||||
// 创建新会话,使用相同的连接参数
|
||||
const newSession = this.createSession(
|
||||
originalSession.host,
|
||||
originalSession.port,
|
||||
originalSession.username,
|
||||
originalSession.password
|
||||
);
|
||||
|
||||
// 自动连接
|
||||
this.connectSession(newSession.id);
|
||||
|
||||
this.showAlert('会话已复制', 'success');
|
||||
}
|
||||
|
||||
// ========== SSH连接管理 ==========
|
||||
async connectSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.connected) {
|
||||
this.showAlert('会话已连接', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 建立WebSocket连接
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/cApi/ssh`;
|
||||
session.websocket = new WebSocket(wsUrl);
|
||||
|
||||
session.websocket.onopen = () => {
|
||||
console.log(`Session ${sessionId} WebSocket连接建立`);
|
||||
this.updateSessionStatus(sessionId, '正在连接SSH...');
|
||||
|
||||
// 发送SSH连接请求
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
host: session.host,
|
||||
port: parseInt(session.port),
|
||||
username: session.username,
|
||||
password: session.password
|
||||
}));
|
||||
};
|
||||
|
||||
session.websocket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleSessionMessage(sessionId, message);
|
||||
};
|
||||
|
||||
session.websocket.onerror = (error) => {
|
||||
console.error(`Session ${sessionId} WebSocket错误:`, error);
|
||||
this.showAlert('WebSocket连接错误', 'danger');
|
||||
session.terminal.writeln('\\r\\n❌ WebSocket连接错误');
|
||||
};
|
||||
|
||||
session.websocket.onclose = () => {
|
||||
console.log(`Session ${sessionId} WebSocket连接关闭`);
|
||||
this.handleSessionDisconnection(sessionId);
|
||||
};
|
||||
|
||||
// 处理终端输入
|
||||
session.terminal.onData((data) => {
|
||||
if (session.connected && session.websocket.readyState === WebSocket.OPEN) {
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'command',
|
||||
command: data
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 处理终端大小变化
|
||||
session.terminal.onResize((size) => {
|
||||
if (session.connected && session.websocket.readyState === WebSocket.OPEN) {
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: size.cols,
|
||||
rows: size.rows
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
this.showAlert('连接失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionMessage(sessionId, message) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
switch (message.type) {
|
||||
case 'connected':
|
||||
session.connected = true;
|
||||
this.updateTabStatus(sessionId, true);
|
||||
this.updateSessionStatus(sessionId, '已连接');
|
||||
|
||||
// 查找服务器ID
|
||||
session.serverId = this.findServerIdByConnection(
|
||||
session.host,
|
||||
session.port,
|
||||
session.username
|
||||
);
|
||||
|
||||
session.terminal.clear();
|
||||
session.terminal.writeln('🎉 SSH连接建立成功!');
|
||||
session.terminal.writeln(`连接到: ${session.name}`);
|
||||
session.terminal.writeln('');
|
||||
|
||||
this.showAlert(`会话 "${session.name}" 连接成功`, 'success');
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
session.terminal.write(message.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
session.terminal.writeln(`\\r\\n❌ 错误: ${message.message}`);
|
||||
this.showAlert(`会话连接失败: ${message.message}`, 'danger');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleSessionDisconnection(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
session.connected = false;
|
||||
session.serverId = null;
|
||||
this.updateTabStatus(sessionId, false);
|
||||
this.updateSessionStatus(sessionId, '已断开连接');
|
||||
|
||||
if (session.terminal) {
|
||||
session.terminal.writeln('\\r\\n🔌 连接已关闭');
|
||||
}
|
||||
|
||||
this.showAlert(`会话 "${session.name}" 已断开连接`, 'warning');
|
||||
}
|
||||
|
||||
disconnectSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
if (session.websocket) {
|
||||
session.websocket.send(JSON.stringify({
|
||||
type: 'disconnect'
|
||||
}));
|
||||
session.websocket.close();
|
||||
}
|
||||
|
||||
this.handleSessionDisconnection(sessionId);
|
||||
}
|
||||
|
||||
updateTabStatus(sessionId, connected) {
|
||||
const tab = document.getElementById(`tab_${sessionId}`);
|
||||
if (!tab) return;
|
||||
|
||||
const statusDot = tab.querySelector('.tab-status');
|
||||
if (connected) {
|
||||
statusDot.classList.remove('disconnected');
|
||||
statusDot.classList.add('connected');
|
||||
} else {
|
||||
statusDot.classList.remove('connected');
|
||||
statusDot.classList.add('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
updateSessionStatus(sessionId, message) {
|
||||
const statusElement = document.getElementById(`status_${sessionId}`);
|
||||
if (statusElement) {
|
||||
statusElement.innerHTML = message.includes('已连接') ?
|
||||
`🟢 ${message}` :
|
||||
`🔴 ${message}`;
|
||||
}
|
||||
|
||||
// 更新状态栏
|
||||
if (sessionId === this.activeSessionId) {
|
||||
document.getElementById('statusBar').textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 服务器配置管理 ==========
|
||||
async loadSavedServers() {
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers');
|
||||
const servers = await response.json();
|
||||
this.savedServers = servers;
|
||||
|
||||
const select = document.getElementById('savedServers');
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
|
||||
select.innerHTML = '<option value="">选择已保存的服务器...</option>';
|
||||
fileServerSelect.innerHTML = '<option value="">选择服务器...</option>';
|
||||
|
||||
servers.forEach(server => {
|
||||
const option = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
select.add(option);
|
||||
|
||||
const fileOption = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
fileServerSelect.add(fileOption);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
findServerIdByConnection(host, port, username) {
|
||||
const matchedServer = this.savedServers.find(server =>
|
||||
server.host === host &&
|
||||
server.port === parseInt(port) &&
|
||||
server.username === username
|
||||
);
|
||||
return matchedServer ? matchedServer.id : null;
|
||||
}
|
||||
|
||||
async loadServerConfig() {
|
||||
const serverId = document.getElementById('savedServers').value;
|
||||
if (!serverId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/servers/${serverId}`);
|
||||
const server = await response.json();
|
||||
|
||||
document.getElementById('host').value = server.host;
|
||||
document.getElementById('port').value = server.port;
|
||||
document.getElementById('username').value = server.username;
|
||||
document.getElementById('serverName').value = server.name;
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器配置失败:', error);
|
||||
this.showAlert('加载服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async saveServerConfig() {
|
||||
const serverData = {
|
||||
name: document.getElementById('serverName').value ||
|
||||
`${document.getElementById('username').value}@${document.getElementById('host').value}`,
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('服务器配置已保存', 'success');
|
||||
this.loadSavedServers();
|
||||
} else {
|
||||
this.showAlert('保存失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存服务器配置失败:', error);
|
||||
this.showAlert('保存服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const originalText = testBtn.innerHTML;
|
||||
testBtn.innerHTML = '<div class="loading"></div> 测试中...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
const serverData = {
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('连接测试成功', 'success');
|
||||
} else {
|
||||
this.showAlert('连接测试失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error);
|
||||
this.showAlert('连接测试失败', 'danger');
|
||||
} finally {
|
||||
testBtn.innerHTML = originalText;
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 文件管理相关 ==========
|
||||
getCurrentServerId() {
|
||||
if (this.activeSessionId) {
|
||||
const session = this.sessions.get(this.activeSessionId);
|
||||
return session ? session.serverId : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 文件管理功能
|
||||
currentFileServerId = null;
|
||||
currentPath = '/';
|
||||
|
||||
async switchFileServer() {
|
||||
this.currentFileServerId = document.getElementById('fileServerSelect').value;
|
||||
if (this.currentFileServerId) {
|
||||
this.currentPath = '/';
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshFiles() {
|
||||
if (!this.currentFileServerId) {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/list/${this.currentFileServerId}?remotePath=${encodeURIComponent(this.currentPath)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.displayFiles(result.files);
|
||||
} else {
|
||||
this.showAlert('获取文件列表失败: ' + result.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${result.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
this.showAlert('获取文件列表失败: ' + error.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
displayFiles(files) {
|
||||
const container = document.getElementById('fileGrid');
|
||||
container.innerHTML = '';
|
||||
|
||||
files.forEach(file => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.onclick = () => this.handleFileClick(file);
|
||||
|
||||
const icon = file.directory ? 'fas fa-folder' : 'fas fa-file';
|
||||
const size = file.directory ? '-' : this.formatFileSize(file.size);
|
||||
const date = new Date(file.lastModified).toLocaleString('zh-CN');
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<i class="${icon} file-icon"></i>
|
||||
<span class="file-name">${file.name}</span>
|
||||
<span class="file-size">${size}</span>
|
||||
<span class="file-date">${date}</span>
|
||||
<div class="file-actions">
|
||||
${!file.directory ? `
|
||||
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); downloadFile('${file.name}')">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); deleteFile('${file.name}', ${file.directory})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
|
||||
async handleFileClick(file) {
|
||||
if (file.directory) {
|
||||
this.currentPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + file.name :
|
||||
this.currentPath + '/' + file.name;
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
}
|
||||
|
||||
async navigateUp() {
|
||||
if (this.currentPath === '/') return;
|
||||
|
||||
const pathParts = this.currentPath.split('/').filter(p => p);
|
||||
pathParts.pop();
|
||||
this.currentPath = '/' + pathParts.join('/');
|
||||
if (this.currentPath !== '/' && !this.currentPath.endsWith('/')) {
|
||||
this.currentPath += '/';
|
||||
}
|
||||
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
console.log('uploadFiles called');
|
||||
|
||||
if (!this.currentFileServerId) {
|
||||
this.showAlert('请先选择一个服务器', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Server selected:', this.currentFileServerId);
|
||||
|
||||
const files = document.getElementById('uploadFiles').files;
|
||||
const uploadPath = document.getElementById('uploadPath').value;
|
||||
|
||||
if (files.length === 0) {
|
||||
this.showAlert('请选择要上传的文件', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (let file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
formData.append('remotePath', uploadPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/upload-batch/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert(`成功上传 ${result.count} 个文件`, 'success');
|
||||
closeModal('uploadModal');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('上传失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
this.showAlert('上传文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(filename) {
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/download/${this.currentFileServerId}?remoteFilePath=${encodeURIComponent(filePath)}`);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
this.showAlert('文件下载成功', 'success');
|
||||
} else {
|
||||
this.showAlert('文件下载失败', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
this.showAlert('下载文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(filename, isDirectory) {
|
||||
if (!confirm(`确定要删除${isDirectory ? '目录' : '文件'} "${filename}" 吗?`)) return;
|
||||
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/delete/${this.currentFileServerId}?remotePath=${encodeURIComponent(filePath)}&isDirectory=${isDirectory}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('删除成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('删除失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.showAlert('删除失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async createFolder() {
|
||||
const folderName = prompt('请输入文件夹名称:');
|
||||
if (!folderName) return;
|
||||
|
||||
const folderPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + folderName :
|
||||
this.currentPath + '/' + folderName;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/mkdir/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ remotePath: folderPath })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('文件夹创建成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('创建失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文件夹失败:', error);
|
||||
this.showAlert('创建文件夹失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UI工具方法 ==========
|
||||
showAlert(message, type) {
|
||||
const container = document.getElementById('alertContainer');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 全局函数 ==========
|
||||
let sshClient = null;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
sshClient = new MultiSessionWebSSHClient();
|
||||
});
|
||||
|
||||
// 页面切换
|
||||
function switchPage(pageName) {
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
const onclick = item.getAttribute('onclick');
|
||||
if (onclick && onclick.includes(`switchPage('${pageName}')`)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.page-content').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`page-${pageName}`).classList.add('active');
|
||||
|
||||
if (pageName === 'files') {
|
||||
sshClient.loadSavedServers().then(() => {
|
||||
// 如果当前有激活的会话,自动选择对应的服务器
|
||||
const currentServerId = sshClient.getCurrentServerId();
|
||||
if (currentServerId) {
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
fileServerSelect.value = currentServerId;
|
||||
|
||||
// 设置文件管理的当前服务器ID并加载文件
|
||||
sshClient.currentFileServerId = currentServerId;
|
||||
sshClient.currentPath = '/';
|
||||
document.getElementById('currentPath').value = sshClient.currentPath;
|
||||
sshClient.refreshFiles(); // 自动加载文件列表
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏折叠
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const title = document.getElementById('sidebarTitle');
|
||||
const navTexts = document.querySelectorAll('.nav-text');
|
||||
|
||||
sidebar.classList.toggle('collapsed');
|
||||
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
title.style.display = 'none';
|
||||
navTexts.forEach(text => text.style.display = 'none');
|
||||
} else {
|
||||
title.style.display = 'inline';
|
||||
navTexts.forEach(text => text.style.display = 'inline');
|
||||
}
|
||||
}
|
||||
|
||||
// SSH连接相关
|
||||
function connectSSH() {
|
||||
const host = document.getElementById('host').value.trim();
|
||||
const port = document.getElementById('port').value.trim();
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
if (!host || !username || !password) {
|
||||
sshClient.showAlert('请填写完整的连接信息', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新会话并连接
|
||||
const session = sshClient.createSession(host, port || 22, username, password);
|
||||
sshClient.connectSession(session.id);
|
||||
|
||||
// 保存服务器配置(如果需要)
|
||||
if (document.getElementById('saveServer').checked) {
|
||||
sshClient.saveServerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectSSH() {
|
||||
if (sshClient.activeSessionId) {
|
||||
sshClient.disconnectSession(sshClient.activeSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
sshClient.testConnection();
|
||||
}
|
||||
|
||||
function loadSavedServers() {
|
||||
sshClient.loadSavedServers();
|
||||
}
|
||||
|
||||
function loadServerConfig() {
|
||||
sshClient.loadServerConfig();
|
||||
}
|
||||
|
||||
// 文件管理相关
|
||||
function switchFileServer() {
|
||||
sshClient.switchFileServer();
|
||||
}
|
||||
|
||||
function refreshFiles() {
|
||||
sshClient.refreshFiles();
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
sshClient.navigateUp();
|
||||
}
|
||||
|
||||
function showUploadModal() {
|
||||
document.getElementById('uploadModal').classList.add('active');
|
||||
document.getElementById('uploadPath').value = sshClient.currentPath || '/';
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
console.log('handleUpload called');
|
||||
try {
|
||||
sshClient.uploadFiles();
|
||||
} catch (error) {
|
||||
console.error('Error in handleUpload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFiles(event) {
|
||||
sshClient.uploadFiles(event);
|
||||
}
|
||||
|
||||
function downloadFile(filename) {
|
||||
sshClient.downloadFile(filename);
|
||||
}
|
||||
|
||||
function deleteFile(filename, isDirectory) {
|
||||
sshClient.deleteFile(filename, isDirectory);
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
sshClient.createFolder();
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
// 点击弹窗背景关闭弹窗
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Escape 关闭弹窗
|
||||
if (event.key === 'Escape') {
|
||||
document.querySelectorAll('.modal.active').forEach(modal => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Ctrl+Enter 快速连接
|
||||
if (event.ctrlKey && event.key === 'Enter' && sshClient.sessions.size === 0) {
|
||||
connectSSH();
|
||||
}
|
||||
|
||||
// Ctrl+T 新建会话 (when connected)
|
||||
if (event.ctrlKey && event.key === 't' && sshClient.activeSessionId) {
|
||||
const currentSession = sshClient.sessions.get(sshClient.activeSessionId);
|
||||
if (currentSession) {
|
||||
sshClient.duplicateSession(sshClient.activeSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+W 关闭当前会话
|
||||
if (event.ctrlKey && event.key === 'w' && sshClient.activeSessionId) {
|
||||
sshClient.closeSession(sshClient.activeSessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时断开所有连接
|
||||
window.addEventListener('beforeunload', function(event) {
|
||||
if (sshClient && sshClient.sessions.size > 0) {
|
||||
sshClient.sessions.forEach((session, sessionId) => {
|
||||
if (session.websocket) {
|
||||
session.websocket.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
800
src/main/resources/static/assets/js/webssh-simple.js
Normal file
800
src/main/resources/static/assets/js/webssh-simple.js
Normal file
@@ -0,0 +1,800 @@
|
||||
/**
|
||||
* Web SSH 简化版客户端
|
||||
* 支持SSH连接和文件管理功能
|
||||
*/
|
||||
|
||||
class SimpleWebSSHClient {
|
||||
constructor() {
|
||||
this.terminal = null;
|
||||
this.websocket = null;
|
||||
this.fitAddon = null;
|
||||
this.connected = false;
|
||||
this.currentServer = null;
|
||||
this.currentServerId = null; // 添加当前服务器ID
|
||||
this.currentFileServerId = null;
|
||||
this.currentPath = '/';
|
||||
this.savedServers = []; // 缓存保存的服务器列表
|
||||
|
||||
this.initializeTerminal();
|
||||
this.loadSavedServers();
|
||||
}
|
||||
|
||||
// ========== 终端初始化 ==========
|
||||
initializeTerminal() {
|
||||
this.terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#ffffff',
|
||||
selection: '#ffffff40'
|
||||
},
|
||||
rows: 30,
|
||||
cols: 120
|
||||
});
|
||||
|
||||
this.fitAddon = new FitAddon.FitAddon();
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
|
||||
this.terminal.open(document.getElementById('terminal'));
|
||||
this.fitAddon.fit();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.fitAddon) {
|
||||
setTimeout(() => this.fitAddon.fit(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新终端统计信息
|
||||
this.terminal.onResize((size) => {
|
||||
document.getElementById('terminalStats').textContent =
|
||||
`行: ${size.rows}, 列: ${size.cols}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ========== SSH连接管理 ==========
|
||||
async connect(host, port, username, password) {
|
||||
if (this.connected) {
|
||||
this.showAlert('已有连接存在,请先断开', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentServer = {
|
||||
host, port, username,
|
||||
name: `${username}@${host}:${port}`
|
||||
};
|
||||
|
||||
try {
|
||||
// 建立WebSocket连接
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ssh`;
|
||||
this.websocket = new WebSocket(wsUrl);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
console.log('WebSocket连接建立');
|
||||
this.updateStatus('正在连接SSH...');
|
||||
|
||||
// 发送SSH连接请求
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'connect',
|
||||
host: host,
|
||||
port: parseInt(port),
|
||||
username: username,
|
||||
password: password
|
||||
}));
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleWebSocketMessage(message);
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
this.showAlert('WebSocket连接错误', 'danger');
|
||||
this.terminal.writeln('\r\n❌ WebSocket连接错误');
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
console.log('WebSocket连接关闭');
|
||||
this.handleDisconnection();
|
||||
};
|
||||
|
||||
// 处理终端输入
|
||||
this.terminal.onData((data) => {
|
||||
if (this.connected && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'command',
|
||||
command: data
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// 处理终端大小变化
|
||||
this.terminal.onResize((size) => {
|
||||
if (this.connected && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: size.cols,
|
||||
rows: size.rows
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
this.showAlert('连接失败: ' + error.message, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
handleWebSocketMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'connected':
|
||||
this.connected = true;
|
||||
this.updateConnectionStatus(true);
|
||||
|
||||
// 查找并设置当前服务器ID
|
||||
this.currentServerId = this.findServerIdByConnection(
|
||||
this.currentServer.host,
|
||||
this.currentServer.port,
|
||||
this.currentServer.username
|
||||
);
|
||||
|
||||
this.terminal.clear();
|
||||
this.terminal.writeln('🎉 SSH连接建立成功!');
|
||||
this.terminal.writeln(`连接到: ${this.currentServer.name}`);
|
||||
this.terminal.writeln('');
|
||||
this.showAlert('SSH连接成功', 'success');
|
||||
this.updateStatus('已连接');
|
||||
|
||||
// 显示终端容器
|
||||
document.getElementById('terminalContainer').classList.remove('hidden');
|
||||
this.fitAddon.fit();
|
||||
|
||||
// 保存服务器配置(如果需要)
|
||||
if (document.getElementById('saveServer').checked) {
|
||||
this.saveServerConfig();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
this.terminal.write(message.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.terminal.writeln(`\r\n❌ 错误: ${message.message}`);
|
||||
this.showAlert(`连接失败: ${message.message}`, 'danger');
|
||||
this.updateStatus('连接失败');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.websocket) {
|
||||
this.websocket.send(JSON.stringify({
|
||||
type: 'disconnect'
|
||||
}));
|
||||
this.websocket.close();
|
||||
}
|
||||
|
||||
this.handleDisconnection();
|
||||
}
|
||||
|
||||
handleDisconnection() {
|
||||
this.connected = false;
|
||||
this.currentServer = null;
|
||||
this.currentServerId = null; // 清除当前服务器ID
|
||||
this.updateConnectionStatus(false);
|
||||
this.updateStatus('已断开连接');
|
||||
|
||||
if (this.terminal) {
|
||||
this.terminal.writeln('\r\n🔌 连接已关闭');
|
||||
}
|
||||
|
||||
document.getElementById('terminalContainer').classList.add('hidden');
|
||||
this.showAlert('SSH连接已断开', 'danger');
|
||||
}
|
||||
|
||||
// ========== 服务器配置管理 ==========
|
||||
async loadSavedServers() {
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers');
|
||||
const servers = await response.json();
|
||||
|
||||
// 缓存服务器列表
|
||||
this.savedServers = servers;
|
||||
|
||||
const select = document.getElementById('savedServers');
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
|
||||
// 清空现有选项
|
||||
select.innerHTML = '<option value="">选择已保存的服务器...</option>';
|
||||
fileServerSelect.innerHTML = '<option value="">选择服务器...</option>';
|
||||
|
||||
servers.forEach(server => {
|
||||
const option = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
select.add(option);
|
||||
|
||||
const fileOption = new Option(`${server.name} (${server.host}:${server.port})`, server.id);
|
||||
fileServerSelect.add(fileOption);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据连接信息查找服务器ID
|
||||
findServerIdByConnection(host, port, username) {
|
||||
const matchedServer = this.savedServers.find(server =>
|
||||
server.host === host &&
|
||||
server.port === parseInt(port) &&
|
||||
server.username === username
|
||||
);
|
||||
return matchedServer ? matchedServer.id : null;
|
||||
}
|
||||
|
||||
async loadServerConfig() {
|
||||
const serverId = document.getElementById('savedServers').value;
|
||||
if (!serverId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/servers/${serverId}`);
|
||||
const server = await response.json();
|
||||
|
||||
document.getElementById('host').value = server.host;
|
||||
document.getElementById('port').value = server.port;
|
||||
document.getElementById('username').value = server.username;
|
||||
document.getElementById('serverName').value = server.name;
|
||||
// 不填充密码,出于安全考虑
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载服务器配置失败:', error);
|
||||
this.showAlert('加载服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async saveServerConfig() {
|
||||
const serverData = {
|
||||
name: document.getElementById('serverName').value ||
|
||||
`${document.getElementById('username').value}@${document.getElementById('host').value}`,
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('服务器配置已保存', 'success');
|
||||
this.loadSavedServers(); // 重新加载列表
|
||||
} else {
|
||||
this.showAlert('保存失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存服务器配置失败:', error);
|
||||
this.showAlert('保存服务器配置失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const originalText = testBtn.innerHTML;
|
||||
testBtn.innerHTML = '<div class="loading"></div> 测试中...';
|
||||
testBtn.disabled = true;
|
||||
|
||||
const serverData = {
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/cApi/api/servers/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('连接测试成功', 'success');
|
||||
} else {
|
||||
this.showAlert('连接测试失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error);
|
||||
this.showAlert('连接测试失败', 'danger');
|
||||
} finally {
|
||||
testBtn.innerHTML = originalText;
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动选择当前连接的服务器并切换到文件管理
|
||||
async switchToFileManagerWithCurrentServer() {
|
||||
if (this.currentServerId) {
|
||||
// 设置文件服务器选择框
|
||||
const fileServerSelect = document.getElementById('fileServerSelect');
|
||||
fileServerSelect.value = this.currentServerId;
|
||||
|
||||
// 切换文件服务器
|
||||
this.currentFileServerId = this.currentServerId;
|
||||
this.currentPath = '/';
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 文件管理 ==========
|
||||
async switchFileServer() {
|
||||
this.currentFileServerId = document.getElementById('fileServerSelect').value;
|
||||
if (this.currentFileServerId) {
|
||||
this.currentPath = '/';
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshFiles() {
|
||||
if (!this.currentFileServerId) {
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
请先选择一个服务器来浏览文件
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/list/${this.currentFileServerId}?remotePath=${encodeURIComponent(this.currentPath)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.displayFiles(result.files);
|
||||
} else {
|
||||
this.showAlert('获取文件列表失败: ' + result.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${result.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error);
|
||||
this.showAlert('获取文件列表失败: ' + error.message, 'danger');
|
||||
document.getElementById('fileGrid').innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
获取文件列表失败: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
displayFiles(files) {
|
||||
const container = document.getElementById('fileGrid');
|
||||
container.innerHTML = '';
|
||||
|
||||
files.forEach(file => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'file-item';
|
||||
fileItem.onclick = () => this.handleFileClick(file);
|
||||
|
||||
const icon = file.directory ? 'fas fa-folder' : 'fas fa-file';
|
||||
const size = file.directory ? '-' : this.formatFileSize(file.size);
|
||||
const date = new Date(file.lastModified).toLocaleString('zh-CN');
|
||||
|
||||
fileItem.innerHTML = `
|
||||
<i class="${icon} file-icon"></i>
|
||||
<span class="file-name">${file.name}</span>
|
||||
<span class="file-size">${size}</span>
|
||||
<span class="file-date">${date}</span>
|
||||
<div class="file-actions">
|
||||
${!file.directory ? `
|
||||
<button class="btn btn-sm btn-success" onclick="event.stopPropagation(); downloadFile('${file.name}')">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); deleteFile('${file.name}', ${file.directory})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fileItem);
|
||||
});
|
||||
}
|
||||
|
||||
async handleFileClick(file) {
|
||||
if (file.directory) {
|
||||
this.currentPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + file.name :
|
||||
this.currentPath + '/' + file.name;
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
}
|
||||
|
||||
async navigateUp() {
|
||||
if (this.currentPath === '/') return;
|
||||
|
||||
const pathParts = this.currentPath.split('/').filter(p => p);
|
||||
pathParts.pop();
|
||||
this.currentPath = '/' + pathParts.join('/');
|
||||
if (this.currentPath !== '/' && !this.currentPath.endsWith('/')) {
|
||||
this.currentPath += '/';
|
||||
}
|
||||
|
||||
document.getElementById('currentPath').value = this.currentPath;
|
||||
await this.refreshFiles();
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
console.log('uploadFiles called');
|
||||
|
||||
// 检查是否已选择服务器
|
||||
if (!this.currentFileServerId) {
|
||||
this.showAlert('请先选择一个服务器', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Server selected:', this.currentFileServerId);
|
||||
|
||||
const files = document.getElementById('uploadFiles').files;
|
||||
const uploadPath = document.getElementById('uploadPath').value;
|
||||
|
||||
if (files.length === 0) {
|
||||
this.showAlert('请选择要上传的文件', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (let file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
formData.append('remotePath', uploadPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/upload-batch/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert(`成功上传 ${result.count} 个文件`, 'success');
|
||||
this.closeModal('uploadModal');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('上传失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
this.showAlert('上传文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(filename) {
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/download/${this.currentFileServerId}?remoteFilePath=${encodeURIComponent(filePath)}`);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
this.showAlert('文件下载成功', 'success');
|
||||
} else {
|
||||
this.showAlert('文件下载失败', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
this.showAlert('下载文件失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(filename, isDirectory) {
|
||||
if (!confirm(`确定要删除${isDirectory ? '目录' : '文件'} "${filename}" 吗?`)) return;
|
||||
|
||||
const filePath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + filename :
|
||||
this.currentPath + '/' + filename;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/delete/${this.currentFileServerId}?remotePath=${encodeURIComponent(filePath)}&isDirectory=${isDirectory}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('删除成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('删除失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
this.showAlert('删除失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async createFolder() {
|
||||
const folderName = prompt('请输入文件夹名称:');
|
||||
if (!folderName) return;
|
||||
|
||||
const folderPath = this.currentPath.endsWith('/') ?
|
||||
this.currentPath + folderName :
|
||||
this.currentPath + '/' + folderName;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/cApi/api/files/mkdir/${this.currentFileServerId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ remotePath: folderPath })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showAlert('文件夹创建成功', 'success');
|
||||
await this.refreshFiles();
|
||||
} else {
|
||||
this.showAlert('创建失败: ' + result.message, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建文件夹失败:', error);
|
||||
this.showAlert('创建文件夹失败', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UI工具方法 ==========
|
||||
updateConnectionStatus(connected) {
|
||||
const statusElement = document.getElementById('connectionStatus');
|
||||
const connectBtn = document.querySelector('button[onclick="connectSSH()"]');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
|
||||
if (connected) {
|
||||
statusElement.innerHTML = `🟢 已连接 - ${this.currentServer.name}`;
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
} else {
|
||||
statusElement.innerHTML = '🔴 未连接';
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(message) {
|
||||
document.getElementById('statusBar').textContent = message;
|
||||
}
|
||||
|
||||
showAlert(message, type) {
|
||||
const container = document.getElementById('alertContainer');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(alert);
|
||||
|
||||
// 5秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 全局函数 ==========
|
||||
let sshClient = null;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
sshClient = new SimpleWebSSHClient();
|
||||
});
|
||||
|
||||
// 页面切换
|
||||
function switchPage(pageName) {
|
||||
// 更新导航状态
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 找到对应的导航项并设为激活状态
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
const onclick = item.getAttribute('onclick');
|
||||
if (onclick && onclick.includes(`switchPage('${pageName}')`)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 切换页面内容
|
||||
document.querySelectorAll('.page-content').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`page-${pageName}`).classList.add('active');
|
||||
|
||||
// 根据页面执行特定操作
|
||||
if (pageName === 'files') {
|
||||
sshClient.loadSavedServers().then(() => {
|
||||
// 如果当前有连接的服务器,自动选择它
|
||||
sshClient.switchToFileManagerWithCurrentServer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏折叠
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const title = document.getElementById('sidebarTitle');
|
||||
const navTexts = document.querySelectorAll('.nav-text');
|
||||
|
||||
sidebar.classList.toggle('collapsed');
|
||||
|
||||
if (sidebar.classList.contains('collapsed')) {
|
||||
title.style.display = 'none';
|
||||
navTexts.forEach(text => text.style.display = 'none');
|
||||
} else {
|
||||
title.style.display = 'inline';
|
||||
navTexts.forEach(text => text.style.display = 'inline');
|
||||
}
|
||||
}
|
||||
|
||||
// SSH连接相关
|
||||
function connectSSH() {
|
||||
const host = document.getElementById('host').value.trim();
|
||||
const port = document.getElementById('port').value.trim();
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
if (!host || !username || !password) {
|
||||
sshClient.showAlert('请填写完整的连接信息', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
sshClient.connect(host, port || 22, username, password);
|
||||
}
|
||||
|
||||
function disconnectSSH() {
|
||||
sshClient.disconnect();
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
sshClient.testConnection();
|
||||
}
|
||||
|
||||
function loadSavedServers() {
|
||||
sshClient.loadSavedServers();
|
||||
}
|
||||
|
||||
function loadServerConfig() {
|
||||
sshClient.loadServerConfig();
|
||||
}
|
||||
|
||||
// 文件管理相关
|
||||
function switchFileServer() {
|
||||
sshClient.switchFileServer();
|
||||
}
|
||||
|
||||
function refreshFiles() {
|
||||
sshClient.refreshFiles();
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
sshClient.navigateUp();
|
||||
}
|
||||
|
||||
function showUploadModal() {
|
||||
document.getElementById('uploadModal').classList.add('active');
|
||||
document.getElementById('uploadPath').value = sshClient.currentPath || '/';
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
console.log('handleUpload called');
|
||||
try {
|
||||
sshClient.uploadFiles();
|
||||
} catch (error) {
|
||||
console.error('Error in handleUpload:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFiles(event) {
|
||||
sshClient.uploadFiles(event);
|
||||
}
|
||||
|
||||
function downloadFile(filename) {
|
||||
sshClient.downloadFile(filename);
|
||||
}
|
||||
|
||||
function deleteFile(filename, isDirectory) {
|
||||
sshClient.deleteFile(filename, isDirectory);
|
||||
}
|
||||
|
||||
function createFolder() {
|
||||
sshClient.createFolder();
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
// 点击弹窗背景关闭弹窗
|
||||
document.addEventListener('click', function(event) {
|
||||
if (event.target.classList.contains('modal')) {
|
||||
event.target.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Escape 关闭弹窗
|
||||
if (event.key === 'Escape') {
|
||||
document.querySelectorAll('.modal.active').forEach(modal => {
|
||||
modal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Ctrl+Enter 快速连接
|
||||
if (event.ctrlKey && event.key === 'Enter' && !sshClient.connected) {
|
||||
connectSSH();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时断开连接
|
||||
window.addEventListener('beforeunload', function(event) {
|
||||
if (sshClient && sshClient.connected) {
|
||||
sshClient.disconnect();
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
1059
src/main/resources/templates/views/ssh/index.html
Normal file
1059
src/main/resources/templates/views/ssh/index.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user