14
src/main/java/com/sftp/manager/SftpManagerApplication.java
Normal file
14
src/main/java/com/sftp/manager/SftpManagerApplication.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.sftp.manager;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableJpaRepositories
|
||||
public class SftpManagerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SftpManagerApplication.class, args);
|
||||
}
|
||||
}
|
||||
26
src/main/java/com/sftp/manager/config/WebConfig.java
Normal file
26
src/main/java/com/sftp/manager/config/WebConfig.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.sftp.manager.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// Spring Boot 默认已经处理了 static 目录,这里可以移除或保留作为额外配置
|
||||
// registry.addResourceHandler("/**")
|
||||
// .addResourceLocations("classpath:/static/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.sftp.manager.controller;
|
||||
|
||||
import com.sftp.manager.dto.ApiResponse;
|
||||
import com.sftp.manager.dto.ConnectionRequest;
|
||||
import com.sftp.manager.model.Connection;
|
||||
import com.sftp.manager.service.ConnectionService;
|
||||
import com.sftp.manager.service.SessionManager;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/connection")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class ConnectionController {
|
||||
|
||||
@Autowired
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@PostMapping("/connect")
|
||||
public ApiResponse<String> connect(@RequestBody ConnectionRequest request) {
|
||||
try {
|
||||
String sessionId = connectionService.connect(request);
|
||||
return ApiResponse.success("连接成功", sessionId);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("连接失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/disconnect")
|
||||
public ApiResponse<Void> disconnect(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String sessionId = request.get("sessionId");
|
||||
connectionService.disconnect(sessionId);
|
||||
return ApiResponse.success("断开成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("断开失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
public ApiResponse<Connection> saveConnection(@RequestBody Connection connection) {
|
||||
try {
|
||||
Connection saved = connectionService.saveConnection(connection);
|
||||
return ApiResponse.success("保存成功", saved);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public ApiResponse<List<Connection>> listConnections() {
|
||||
try {
|
||||
List<Connection> connections = connectionService.listConnections();
|
||||
return ApiResponse.success("查询成功", connections);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<Connection> getConnection(@PathVariable Long id) {
|
||||
try {
|
||||
Connection connection = connectionService.getConnectionById(id);
|
||||
if (connection != null) {
|
||||
return ApiResponse.success("查询成功", connection);
|
||||
} else {
|
||||
return ApiResponse.error("连接不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Void> deleteConnection(@PathVariable Long id) {
|
||||
try {
|
||||
connectionService.deleteConnection(id);
|
||||
return ApiResponse.success("删除成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/active")
|
||||
public ApiResponse<Map<String, Connection>> getActiveConnections() {
|
||||
try {
|
||||
return ApiResponse.success("查询成功", sessionManager.getAllActiveConnections());
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
361
src/main/java/com/sftp/manager/controller/FileController.java
Normal file
361
src/main/java/com/sftp/manager/controller/FileController.java
Normal file
@@ -0,0 +1,361 @@
|
||||
package com.sftp.manager.controller;
|
||||
|
||||
import com.jcraft.jsch.ChannelSftp;
|
||||
import com.sftp.manager.dto.ApiResponse;
|
||||
import com.sftp.manager.dto.BatchDeleteResult;
|
||||
import com.sftp.manager.dto.FileListRequest;
|
||||
import com.sftp.manager.dto.FileOperationRequest;
|
||||
import com.sftp.manager.dto.TransferRequest;
|
||||
import com.sftp.manager.model.FileInfo;
|
||||
import com.sftp.manager.service.LocalFileService;
|
||||
import com.sftp.manager.service.SessionManager;
|
||||
import com.sftp.manager.service.SftpService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class FileController {
|
||||
|
||||
@Autowired
|
||||
private LocalFileService localFileService;
|
||||
|
||||
@Autowired
|
||||
private SftpService sftpService;
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@PostMapping("/list")
|
||||
public ApiResponse<List<FileInfo>> listFiles(@RequestBody FileListRequest request) {
|
||||
try {
|
||||
String sessionId = request.getSessionId();
|
||||
String path = request.getPath();
|
||||
boolean showHidden = Boolean.TRUE.equals(request.getShowHidden());
|
||||
|
||||
List<FileInfo> files;
|
||||
if ("local".equals(sessionId)) {
|
||||
files = localFileService.listFiles(path, showHidden);
|
||||
} else {
|
||||
files = sftpService.listFiles(sessionId, path, showHidden);
|
||||
}
|
||||
|
||||
return ApiResponse.success("查询成功", files);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("列出文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/info")
|
||||
public ApiResponse<FileInfo> getFileInfo(@RequestBody FileOperationRequest request) {
|
||||
try {
|
||||
String sessionId = request.getSessionId();
|
||||
String path = request.getPath();
|
||||
|
||||
FileInfo fileInfo;
|
||||
if ("local".equals(sessionId)) {
|
||||
fileInfo = localFileService.getFileInfo(path);
|
||||
} else {
|
||||
fileInfo = sftpService.getFileInfo(sessionId, path);
|
||||
}
|
||||
|
||||
return ApiResponse.success("查询成功", fileInfo);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("获取文件信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/path")
|
||||
public ApiResponse<Map<String, String>> getCurrentPath(@RequestParam String sessionId) {
|
||||
try {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if ("local".equals(sessionId)) {
|
||||
result.put("path", System.getProperty("user.home"));
|
||||
} else {
|
||||
String path = sftpService.pwd(sessionId);
|
||||
result.put("path", path);
|
||||
}
|
||||
return ApiResponse.success("查询成功", result);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("获取路径失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
@PostMapping("/upload")
|
||||
public ApiResponse<Void> uploadFile(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("targetSessionId") String targetSessionId,
|
||||
@RequestParam("targetPath") String targetPath) {
|
||||
try {
|
||||
if ("local".equals(targetSessionId)) {
|
||||
// 上传到本地
|
||||
File destFile = new File(targetPath, file.getOriginalFilename());
|
||||
file.transferTo(destFile);
|
||||
} else {
|
||||
// 上传到SFTP
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
String remotePath = targetPath.endsWith("/") ?
|
||||
targetPath + file.getOriginalFilename() :
|
||||
targetPath + "/" + file.getOriginalFilename();
|
||||
sftpService.uploadFile(targetSessionId, inputStream, remotePath, file.getSize());
|
||||
}
|
||||
}
|
||||
return ApiResponse.success("上传成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("上传失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
@GetMapping("/download")
|
||||
public void downloadFile(@RequestParam String sessionId,
|
||||
@RequestParam String path,
|
||||
HttpServletResponse response) {
|
||||
try {
|
||||
if ("local".equals(sessionId)) {
|
||||
// 下载本地文件
|
||||
File file = new File(path);
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
|
||||
response.setContentLengthLong(file.length());
|
||||
|
||||
try (InputStream inputStream = new FileInputStream(file);
|
||||
OutputStream outputStream = response.getOutputStream()) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
} else {
|
||||
// 下载SFTP文件
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
FileInfo fileInfo = sftpService.getFileInfo(sessionId, path);
|
||||
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + URLEncoder.encode(fileInfo.getName(), "UTF-8"));
|
||||
response.setContentLengthLong(fileInfo.getSize());
|
||||
|
||||
try (OutputStream outputStream = response.getOutputStream()) {
|
||||
sftpService.downloadFile(sessionId, path, outputStream);
|
||||
outputStream.flush();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
try {
|
||||
response.getWriter().write("下载失败: " + e.getMessage());
|
||||
} catch (Exception ex) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器间传输
|
||||
@PostMapping("/transfer")
|
||||
public ApiResponse<Void> transferFiles(@RequestBody TransferRequest request) {
|
||||
try {
|
||||
String sourceSessionId = request.getSourceSessionId();
|
||||
String sourcePath = request.getSourcePath();
|
||||
String targetSessionId = request.getTargetSessionId();
|
||||
String targetPath = request.getTargetPath();
|
||||
|
||||
// 获取源文件名
|
||||
String fileName;
|
||||
if ("local".equals(sourceSessionId)) {
|
||||
File file = new File(sourcePath);
|
||||
fileName = file.getName();
|
||||
} else {
|
||||
FileInfo fileInfo = sftpService.getFileInfo(sourceSessionId, sourcePath);
|
||||
fileName = fileInfo.getName();
|
||||
}
|
||||
|
||||
// 构建目标路径
|
||||
String finalTargetPath = targetPath.endsWith("/") ?
|
||||
targetPath + fileName :
|
||||
targetPath + "/" + fileName;
|
||||
|
||||
if ("local".equals(sourceSessionId) && "local".equals(targetSessionId)) {
|
||||
// 本地到本地
|
||||
Files.copy(new File(sourcePath).toPath(), new File(finalTargetPath).toPath());
|
||||
} else if ("local".equals(sourceSessionId)) {
|
||||
// 本地到SFTP
|
||||
localFileService.uploadToSftp(sourcePath, targetSessionId, finalTargetPath, sftpService);
|
||||
} else if ("local".equals(targetSessionId)) {
|
||||
// SFTP到本地
|
||||
localFileService.downloadFromSftp(sourceSessionId, sourcePath, finalTargetPath, sftpService);
|
||||
} else {
|
||||
// SFTP到SFTP
|
||||
sftpService.transferBetweenSftp(sourceSessionId, sourcePath,
|
||||
targetSessionId, finalTargetPath);
|
||||
}
|
||||
|
||||
return ApiResponse.success("传输成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("传输失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个文件
|
||||
@DeleteMapping("/delete")
|
||||
public ApiResponse<Void> deleteFile(@RequestParam String sessionId,
|
||||
@RequestParam String path) {
|
||||
try {
|
||||
if ("local".equals(sessionId)) {
|
||||
localFileService.deleteFile(path);
|
||||
} else {
|
||||
sftpService.deleteFile(sessionId, path);
|
||||
}
|
||||
return ApiResponse.success("删除成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 创建目录(单级)
|
||||
@PostMapping("/mkdir")
|
||||
public ApiResponse<Void> createDirectory(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String sessionId = request.get("sessionId");
|
||||
String path = request.get("path");
|
||||
|
||||
if (path == null || path.isEmpty()) {
|
||||
return ApiResponse.error("路径不能为空");
|
||||
}
|
||||
|
||||
boolean result;
|
||||
if ("local".equals(sessionId)) {
|
||||
result = localFileService.createDirectory(path);
|
||||
} else {
|
||||
result = sftpService.createDirectory(sessionId, path);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return ApiResponse.success("创建成功", null);
|
||||
} else {
|
||||
return ApiResponse.error("创建失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("创建失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 创建多级目录
|
||||
@PostMapping("/mkdir-p")
|
||||
public ApiResponse<Void> createDirectories(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String sessionId = request.get("sessionId");
|
||||
String path = request.get("path");
|
||||
|
||||
if (path == null || path.isEmpty()) {
|
||||
return ApiResponse.error("路径不能为空");
|
||||
}
|
||||
|
||||
boolean result;
|
||||
if ("local".equals(sessionId)) {
|
||||
result = localFileService.createDirectories(path);
|
||||
} else {
|
||||
result = sftpService.createDirectories(sessionId, path);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return ApiResponse.success("创建成功", null);
|
||||
} else {
|
||||
return ApiResponse.error("创建失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("创建失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名文件
|
||||
@PostMapping("/rename")
|
||||
public ApiResponse<Void> renameFile(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String sessionId = request.get("sessionId");
|
||||
String oldPath = request.get("oldPath");
|
||||
String newName = request.get("newName");
|
||||
|
||||
if (newName == null || newName.trim().isEmpty()) {
|
||||
return ApiResponse.error("新文件名不能为空");
|
||||
}
|
||||
newName = newName.trim();
|
||||
|
||||
String newPath;
|
||||
if ("local".equals(sessionId)) {
|
||||
File oldFile = new File(oldPath);
|
||||
File parentDir = oldFile.getParentFile();
|
||||
if (parentDir == null) {
|
||||
return ApiResponse.error("无法获取父目录");
|
||||
}
|
||||
newPath = parentDir.getPath() + File.separator + newName;
|
||||
localFileService.renameFile(oldPath, newPath);
|
||||
} else {
|
||||
String parentPath = getParentPathFromString(oldPath);
|
||||
newPath = parentPath.endsWith("/") ? parentPath + newName : parentPath + "/" + newName;
|
||||
sftpService.renameFile(sessionId, oldPath, newPath);
|
||||
}
|
||||
|
||||
return ApiResponse.success("重命名成功", null);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("重命名失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 从路径字符串获取父路径(用于 SFTP 路径)
|
||||
private String getParentPathFromString(String path) {
|
||||
if (path == null || path.isEmpty()) return "/";
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substring(0, path.length() - 1);
|
||||
}
|
||||
int index = path.lastIndexOf("/");
|
||||
if (index <= 0) {
|
||||
return "/";
|
||||
}
|
||||
return path.substring(0, index);
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
@PostMapping("/batch-delete")
|
||||
public ApiResponse<BatchDeleteResult> batchDelete(@RequestBody Map<String, Object> request) {
|
||||
try {
|
||||
String sessionId = (String) request.get("sessionId");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> paths = (List<String>) request.get("paths");
|
||||
if (paths == null) {
|
||||
paths = new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
BatchDeleteResult result;
|
||||
if ("local".equals(sessionId)) {
|
||||
result = localFileService.batchDelete(paths);
|
||||
} else {
|
||||
result = sftpService.batchDelete(sessionId, paths);
|
||||
}
|
||||
|
||||
return ApiResponse.success("删除完成", result);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error("批量删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/main/java/com/sftp/manager/dto/ApiResponse.java
Normal file
41
src/main/java/com/sftp/manager/dto/ApiResponse.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.sftp.manager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
private boolean success; // 操作是否成功
|
||||
private String message; // 响应消息
|
||||
private T data; // 响应数据
|
||||
private String error; // 错误信息
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setSuccess(true);
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setSuccess(true);
|
||||
response.setMessage(message);
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setSuccess(false);
|
||||
response.setMessage(message);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message, String error) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setSuccess(false);
|
||||
response.setMessage(message);
|
||||
response.setError(error);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/sftp/manager/dto/BatchDeleteResult.java
Normal file
13
src/main/java/com/sftp/manager/dto/BatchDeleteResult.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.sftp.manager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BatchDeleteResult {
|
||||
private int successCount;
|
||||
private int failCount;
|
||||
private List<String> failedFiles = new ArrayList<>();
|
||||
}
|
||||
16
src/main/java/com/sftp/manager/dto/ConnectionRequest.java
Normal file
16
src/main/java/com/sftp/manager/dto/ConnectionRequest.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.sftp.manager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ConnectionRequest {
|
||||
private Long id; // 连接ID(用于保存配置)
|
||||
private String name; // 连接名称
|
||||
private String host; // 主机地址
|
||||
private Integer port; // 端口
|
||||
private String username; // 用户名
|
||||
private String password; // 密码
|
||||
private String privateKeyPath; // 私钥路径
|
||||
private String passPhrase; // 私钥密码
|
||||
private String rootPath; // 默认登录后路径
|
||||
}
|
||||
10
src/main/java/com/sftp/manager/dto/FileListRequest.java
Normal file
10
src/main/java/com/sftp/manager/dto/FileListRequest.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.sftp.manager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FileListRequest {
|
||||
private String sessionId; // 会话ID("local"表示本地,否则为SFTP连接ID)
|
||||
private String path; // 要浏览的目录路径
|
||||
private Boolean showHidden; // 是否展示隐藏文件(默认 false)
|
||||
}
|
||||
12
src/main/java/com/sftp/manager/dto/FileOperationRequest.java
Normal file
12
src/main/java/com/sftp/manager/dto/FileOperationRequest.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.sftp.manager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FileOperationRequest {
|
||||
private String sessionId; // 会话ID(标识是本地还是哪个SFTP连接)
|
||||
private String path; // 操作的文件路径
|
||||
private String newName; // 新文件名(重命名使用)
|
||||
private String targetPath; // 目标路径(移动/复制使用)
|
||||
private String targetSessionId; // 目标会话ID(跨服务器传输使用)
|
||||
}
|
||||
11
src/main/java/com/sftp/manager/dto/TransferRequest.java
Normal file
11
src/main/java/com/sftp/manager/dto/TransferRequest.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.sftp.manager.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TransferRequest {
|
||||
private String sourceSessionId; // 源会话ID
|
||||
private String sourcePath; // 源文件路径
|
||||
private String targetSessionId; // 目标会话ID
|
||||
private String targetPath; // 目标路径
|
||||
}
|
||||
59
src/main/java/com/sftp/manager/model/Connection.java
Normal file
59
src/main/java/com/sftp/manager/model/Connection.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.sftp.manager.model;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "connections")
|
||||
public class Connection {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id; // 连接ID(主键)
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name; // 连接名称(用户自定义)
|
||||
|
||||
@Column(nullable = false)
|
||||
private String host; // SFTP服务器地址
|
||||
|
||||
private Integer port; // SFTP端口(默认22)
|
||||
|
||||
@Column(nullable = false)
|
||||
private String username; // 用户名
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String password; // 密码(加密存储)
|
||||
|
||||
private String privateKeyPath; // 私钥路径(可选)
|
||||
|
||||
private String passPhrase; // 私钥密码(可选)
|
||||
|
||||
private Integer connectTimeout; // 连接超时时间
|
||||
|
||||
private String rootPath; // 默认登录后路径
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt; // 创建时间
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt; // 更新时间
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
if (port == null) {
|
||||
port = 22;
|
||||
}
|
||||
if (connectTimeout == null) {
|
||||
connectTimeout = 10000;
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/sftp/manager/model/FileInfo.java
Normal file
18
src/main/java/com/sftp/manager/model/FileInfo.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.sftp.manager.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class FileInfo {
|
||||
private String name; // 文件名
|
||||
private String path; // 完整路径
|
||||
private long size; // 文件大小(字节)
|
||||
|
||||
@JsonProperty("isDirectory")
|
||||
private boolean isDirectory; // 是否为目录
|
||||
|
||||
private LocalDateTime modifiedTime; // 修改时间
|
||||
private String permissions; // 文件权限(如:-rw-r--r--)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sftp.manager.repository;
|
||||
|
||||
import com.sftp.manager.model.Connection;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ConnectionRepository extends JpaRepository<Connection, Long> {
|
||||
List<Connection> findByOrderByCreatedAtDesc(); // 按创建时间倒序查询
|
||||
Optional<Connection> findByName(String name); // 按名称查询
|
||||
}
|
||||
156
src/main/java/com/sftp/manager/service/ConnectionService.java
Normal file
156
src/main/java/com/sftp/manager/service/ConnectionService.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import com.jcraft.jsch.Channel;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.sftp.manager.dto.ConnectionRequest;
|
||||
import com.sftp.manager.model.Connection;
|
||||
import com.sftp.manager.repository.ConnectionRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ConnectionService {
|
||||
|
||||
@Autowired
|
||||
private ConnectionRepository connectionRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@Value("${app.sftp.connection-timeout:10000}")
|
||||
private int connectionTimeout;
|
||||
|
||||
@Value("${app.sftp.max-retries:3}")
|
||||
private int maxRetries;
|
||||
|
||||
public String connect(ConnectionRequest request) throws Exception {
|
||||
JSch jsch = new JSch();
|
||||
Session session = null;
|
||||
Channel channel = null;
|
||||
com.jcraft.jsch.ChannelSftp sftpChannel = null;
|
||||
|
||||
int retryCount = 0;
|
||||
Exception lastException = null;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
// 配置私钥(如果提供)
|
||||
if (request.getPrivateKeyPath() != null && !request.getPrivateKeyPath().isEmpty()) {
|
||||
jsch.addIdentity(request.getPrivateKeyPath(),
|
||||
request.getPassPhrase() != null ? request.getPassPhrase() : "");
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
session = jsch.getSession(request.getUsername(),
|
||||
request.getHost(),
|
||||
request.getPort() != null ? request.getPort() : 22);
|
||||
|
||||
// 配置密码(如果使用密码认证)
|
||||
if (request.getPassword() != null && !request.getPassword().isEmpty()) {
|
||||
session.setPassword(request.getPassword());
|
||||
}
|
||||
|
||||
// 跳过主机密钥验证
|
||||
java.util.Properties config = new java.util.Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
session.setConfig(config);
|
||||
|
||||
// 设置超时
|
||||
session.setTimeout(connectionTimeout);
|
||||
|
||||
// 连接
|
||||
session.connect();
|
||||
channel = session.openChannel("sftp");
|
||||
channel.connect();
|
||||
sftpChannel = (com.jcraft.jsch.ChannelSftp) channel;
|
||||
|
||||
// 如果指定了默认路径,切换到该路径
|
||||
if (request.getRootPath() != null && !request.getRootPath().isEmpty()) {
|
||||
try {
|
||||
sftpChannel.cd(request.getRootPath());
|
||||
} catch (Exception e) {
|
||||
// 路径不存在,使用默认路径
|
||||
}
|
||||
}
|
||||
|
||||
// 创建连接对象(用于保存配置)
|
||||
Connection connection = new Connection();
|
||||
connection.setName(request.getName());
|
||||
connection.setHost(request.getHost());
|
||||
connection.setPort(request.getPort() != null ? request.getPort() : 22);
|
||||
connection.setUsername(request.getUsername());
|
||||
connection.setPassword(request.getPassword());
|
||||
connection.setPrivateKeyPath(request.getPrivateKeyPath());
|
||||
connection.setPassPhrase(request.getPassPhrase());
|
||||
connection.setRootPath(request.getRootPath());
|
||||
connection.setConnectTimeout(connectionTimeout);
|
||||
|
||||
// 添加到会话管理器
|
||||
return sessionManager.addSession(sftpChannel, connection);
|
||||
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
retryCount++;
|
||||
|
||||
// 清理资源
|
||||
if (channel != null && channel.isConnected()) {
|
||||
try {
|
||||
channel.disconnect();
|
||||
} catch (Exception ex) {
|
||||
// 忽略关闭异常
|
||||
}
|
||||
}
|
||||
if (session != null && session.isConnected()) {
|
||||
try {
|
||||
session.disconnect();
|
||||
} catch (Exception ex) {
|
||||
// 忽略关闭异常
|
||||
}
|
||||
}
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
throw new Exception("连接失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 重置变量以便下次重试
|
||||
session = null;
|
||||
channel = null;
|
||||
sftpChannel = null;
|
||||
|
||||
try {
|
||||
Thread.sleep(1000); // 等待1秒后重试
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new Exception("连接中断", ie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("连接失败", lastException);
|
||||
}
|
||||
|
||||
public void disconnect(String sessionId) {
|
||||
sessionManager.removeSession(sessionId);
|
||||
}
|
||||
|
||||
public Connection saveConnection(Connection connection) {
|
||||
return connectionRepository.save(connection);
|
||||
}
|
||||
|
||||
public List<Connection> listConnections() {
|
||||
return connectionRepository.findByOrderByCreatedAtDesc();
|
||||
}
|
||||
|
||||
public Connection getConnectionById(Long id) {
|
||||
return connectionRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
public void deleteConnection(Long id) {
|
||||
connectionRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
358
src/main/java/com/sftp/manager/service/LocalFileService.java
Normal file
358
src/main/java/com/sftp/manager/service/LocalFileService.java
Normal file
@@ -0,0 +1,358 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import com.sftp.manager.dto.BatchDeleteResult;
|
||||
import com.sftp.manager.model.FileInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class LocalFileService {
|
||||
|
||||
public List<FileInfo> listFiles(String path) throws Exception {
|
||||
return listFiles(path, false);
|
||||
}
|
||||
|
||||
public List<FileInfo> listFiles(String path, boolean showHidden) throws Exception {
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
File directory = new File(path);
|
||||
|
||||
if (!directory.exists() || !directory.isDirectory()) {
|
||||
throw new Exception("目录不存在: " + path);
|
||||
}
|
||||
|
||||
File[] fileArray = directory.listFiles();
|
||||
if (fileArray != null) {
|
||||
for (File file : fileArray) {
|
||||
// 不展示隐藏文件时:过滤系统隐藏属性 + 以.开头的名称(Unix 惯例,Windows 上常见)
|
||||
if (!showHidden && (file.isHidden() || file.getName().startsWith("."))) {
|
||||
continue;
|
||||
}
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(file.getName());
|
||||
fileInfo.setPath(file.getAbsolutePath());
|
||||
fileInfo.setSize(file.length());
|
||||
fileInfo.setDirectory(file.isDirectory());
|
||||
|
||||
// 获取修改时间
|
||||
BasicFileAttributes attrs = Files.readAttributes(
|
||||
file.toPath(), BasicFileAttributes.class);
|
||||
fileInfo.setModifiedTime(LocalDateTime.ofInstant(
|
||||
attrs.lastModifiedTime().toInstant(),
|
||||
ZoneId.systemDefault()));
|
||||
|
||||
// 设置权限(仅Windows)
|
||||
if (file.canRead() && file.canWrite()) {
|
||||
fileInfo.setPermissions("-rw-r--r--");
|
||||
} else if (file.canRead()) {
|
||||
fileInfo.setPermissions("-r--r--r--");
|
||||
}
|
||||
|
||||
files.add(fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public boolean fileExists(String path) {
|
||||
return new File(path).exists();
|
||||
}
|
||||
|
||||
public FileInfo getFileInfo(String path) throws Exception {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
throw new Exception("文件不存在: " + path);
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(file.getName());
|
||||
fileInfo.setPath(file.getAbsolutePath());
|
||||
fileInfo.setSize(file.length());
|
||||
fileInfo.setDirectory(file.isDirectory());
|
||||
|
||||
BasicFileAttributes attrs = Files.readAttributes(
|
||||
file.toPath(), BasicFileAttributes.class);
|
||||
fileInfo.setModifiedTime(LocalDateTime.ofInstant(
|
||||
attrs.lastModifiedTime().toInstant(),
|
||||
ZoneId.systemDefault()));
|
||||
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
public String getParentPath(String path) {
|
||||
File file = new File(path);
|
||||
String parent = file.getParent();
|
||||
return parent;
|
||||
}
|
||||
|
||||
// 上传本地文件到SFTP
|
||||
public void uploadToSftp(String localPath, String sessionId,
|
||||
String remotePath, SftpService sftpService) throws Exception {
|
||||
File file = new File(localPath);
|
||||
if (!file.exists()) {
|
||||
throw new Exception("本地文件不存在: " + localPath);
|
||||
}
|
||||
|
||||
try (InputStream inputStream = new FileInputStream(file)) {
|
||||
sftpService.uploadFile(sessionId, inputStream, remotePath, file.length());
|
||||
}
|
||||
}
|
||||
|
||||
// 从SFTP下载到本地
|
||||
public void downloadFromSftp(String sessionId, String remotePath,
|
||||
String localPath, SftpService sftpService) throws Exception {
|
||||
File file = new File(localPath);
|
||||
File parentDir = file.getParentFile();
|
||||
|
||||
// 确保父目录存在
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
try (OutputStream outputStream = new FileOutputStream(file)) {
|
||||
sftpService.downloadFile(sessionId, remotePath, outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个文件或目录
|
||||
public boolean deleteFile(String path) throws Exception {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
throw new Exception("文件不存在: " + path);
|
||||
}
|
||||
checkDeletePermission(file);
|
||||
if (file.isDirectory()) {
|
||||
return deleteDirectory(file);
|
||||
} else {
|
||||
return file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// 递归删除目录
|
||||
private boolean deleteDirectory(File directory) throws Exception {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
} else {
|
||||
if (!file.delete()) {
|
||||
throw new Exception("删除文件失败: " + file.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return directory.delete();
|
||||
}
|
||||
|
||||
// 检查删除权限(本地)
|
||||
private void checkDeletePermission(File file) throws Exception {
|
||||
if (!file.canWrite()) {
|
||||
throw new Exception("没有删除权限: " + file.getPath());
|
||||
}
|
||||
String systemPaths = "C:\\Windows,C:\\Program Files,C:\\Program Files (x86),C:\\System32";
|
||||
String[] paths = systemPaths.split(",");
|
||||
for (String systemPath : paths) {
|
||||
String p = systemPath.trim().toLowerCase();
|
||||
if (p.isEmpty()) continue;
|
||||
if (file.getPath().toLowerCase().startsWith(p)) {
|
||||
throw new Exception("系统目录禁止删除: " + file.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名文件
|
||||
public boolean renameFile(String oldPath, String newPath) throws Exception {
|
||||
File oldFile = new File(oldPath);
|
||||
File newFile = new File(newPath);
|
||||
|
||||
if (!oldFile.exists()) {
|
||||
throw new Exception("源文件不存在: " + oldPath);
|
||||
}
|
||||
|
||||
if (newFile.exists()) {
|
||||
throw new Exception("目标文件已存在: " + newPath);
|
||||
}
|
||||
|
||||
String newFileName = newFile.getName();
|
||||
if (!isValidFileName(newFileName)) {
|
||||
throw new Exception("文件名包含非法字符: " + newFileName);
|
||||
}
|
||||
|
||||
boolean result = oldFile.renameTo(newFile);
|
||||
if (!result) {
|
||||
throw new Exception("重命名失败");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证文件名是否有效
|
||||
private boolean isValidFileName(String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String illegalChars = "\\/:*?\"<>|";
|
||||
for (char c : illegalChars.toCharArray()) {
|
||||
if (fileName.indexOf(c) != -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName.length() > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String upperName = fileName.toUpperCase();
|
||||
String[] reservedNames = {"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"};
|
||||
for (String reserved : reservedNames) {
|
||||
if (upperName.equals(reserved)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
public String getFileExtension(String fileName) {
|
||||
int index = fileName.lastIndexOf('.');
|
||||
if (index == -1 || index == fileName.length() - 1) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(index);
|
||||
}
|
||||
|
||||
// 获取文件名(不带扩展名)
|
||||
public String getFileNameWithoutExtension(String fileName) {
|
||||
int index = fileName.lastIndexOf('.');
|
||||
if (index == -1) {
|
||||
return fileName;
|
||||
}
|
||||
return fileName.substring(0, index);
|
||||
}
|
||||
|
||||
// 创建目录(单级,父目录必须存在)
|
||||
public boolean createDirectory(String path) throws Exception {
|
||||
File directory = new File(path);
|
||||
|
||||
if (directory.exists()) {
|
||||
throw new Exception("目录已存在: " + path);
|
||||
}
|
||||
|
||||
String dirName = directory.getName();
|
||||
if (!isValidDirectoryName(dirName)) {
|
||||
throw new Exception("目录名包含非法字符或无效: " + dirName);
|
||||
}
|
||||
|
||||
File parentDir = directory.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
throw new Exception("父目录不存在: " + parentDir.getPath());
|
||||
}
|
||||
|
||||
boolean result = directory.mkdirs();
|
||||
if (!result) {
|
||||
throw new Exception("创建目录失败");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建多级目录
|
||||
public boolean createDirectories(String path) throws Exception {
|
||||
File directory = new File(path);
|
||||
|
||||
if (directory.exists()) {
|
||||
throw new Exception("目录已存在: " + path);
|
||||
}
|
||||
|
||||
String[] parts = path.replace('\\', '/').split("/");
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty()) continue;
|
||||
if (!isValidDirectoryName(part)) {
|
||||
throw new Exception("路径包含非法字符: " + part);
|
||||
}
|
||||
}
|
||||
|
||||
boolean result = directory.mkdirs();
|
||||
if (!result) {
|
||||
throw new Exception("创建目录失败");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证目录名是否有效
|
||||
private boolean isValidDirectoryName(String dirName) {
|
||||
if (dirName == null || dirName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dirName = dirName.trim();
|
||||
|
||||
String illegalChars = "\\/:*?\"<>|";
|
||||
for (char c : illegalChars.toCharArray()) {
|
||||
if (dirName.indexOf(c) != -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (dirName.startsWith(".") || dirName.endsWith(".")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dirName.length() > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String upperName = dirName.toUpperCase();
|
||||
String[] reservedNames = {"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"};
|
||||
for (String reserved : reservedNames) {
|
||||
if (upperName.equals(reserved)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
public BatchDeleteResult batchDelete(List<String> paths) {
|
||||
BatchDeleteResult result = new BatchDeleteResult();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
List<String> failedFiles = new ArrayList<>();
|
||||
|
||||
for (String path : paths) {
|
||||
try {
|
||||
deleteFile(path);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
failedFiles.add(path + " - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setFailedFiles(failedFiles);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
56
src/main/java/com/sftp/manager/service/SessionManager.java
Normal file
56
src/main/java/com/sftp/manager/service/SessionManager.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import com.jcraft.jsch.ChannelSftp;
|
||||
import com.sftp.manager.model.Connection;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class SessionManager {
|
||||
|
||||
private final Map<String, ChannelSftp> activeSessions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Connection> sessionConnections = new ConcurrentHashMap<>();
|
||||
|
||||
public String addSession(ChannelSftp channel, Connection connection) {
|
||||
String sessionId = "sftp-" + UUID.randomUUID().toString();
|
||||
activeSessions.put(sessionId, channel);
|
||||
sessionConnections.put(sessionId, connection);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public ChannelSftp getSession(String sessionId) {
|
||||
return activeSessions.get(sessionId);
|
||||
}
|
||||
|
||||
public Connection getConnection(String sessionId) {
|
||||
return sessionConnections.get(sessionId);
|
||||
}
|
||||
|
||||
public void removeSession(String sessionId) {
|
||||
ChannelSftp channel = activeSessions.get(sessionId);
|
||||
if (channel != null) {
|
||||
try {
|
||||
channel.disconnect();
|
||||
} catch (Exception e) {
|
||||
// 忽略关闭异常
|
||||
}
|
||||
}
|
||||
activeSessions.remove(sessionId);
|
||||
sessionConnections.remove(sessionId);
|
||||
}
|
||||
|
||||
public boolean isActive(String sessionId) {
|
||||
return activeSessions.containsKey(sessionId);
|
||||
}
|
||||
|
||||
public Map<String, Connection> getAllActiveConnections() {
|
||||
return new ConcurrentHashMap<>(sessionConnections);
|
||||
}
|
||||
|
||||
public int getActiveSessionCount() {
|
||||
return activeSessions.size();
|
||||
}
|
||||
}
|
||||
384
src/main/java/com/sftp/manager/service/SftpService.java
Normal file
384
src/main/java/com/sftp/manager/service/SftpService.java
Normal file
@@ -0,0 +1,384 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import com.jcraft.jsch.ChannelSftp;
|
||||
import com.jcraft.jsch.SftpATTRS;
|
||||
import com.jcraft.jsch.SftpException;
|
||||
import com.sftp.manager.dto.BatchDeleteResult;
|
||||
import com.sftp.manager.model.FileInfo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
@Service
|
||||
public class SftpService {
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
|
||||
public List<FileInfo> listFiles(String sessionId, String path) throws Exception {
|
||||
return listFiles(sessionId, path, false);
|
||||
}
|
||||
|
||||
public List<FileInfo> listFiles(String sessionId, String path, boolean showHidden) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
Vector<?> entries = channel.ls(path);
|
||||
|
||||
for (Object obj : entries) {
|
||||
ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) obj;
|
||||
String fileName = entry.getFilename();
|
||||
|
||||
// 跳过.和..
|
||||
if (".".equals(fileName) || "..".equals(fileName)) {
|
||||
continue;
|
||||
}
|
||||
// 不展示隐藏文件时跳过以.开头的文件/目录(Unix 惯例)
|
||||
if (!showHidden && fileName.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(fileName);
|
||||
fileInfo.setPath(path.endsWith("/") ? path + fileName : path + "/" + fileName);
|
||||
fileInfo.setSize(entry.getAttrs().getSize());
|
||||
fileInfo.setDirectory(entry.getAttrs().isDir());
|
||||
|
||||
// 获取修改时间
|
||||
int mtime = entry.getAttrs().getMTime();
|
||||
fileInfo.setModifiedTime(LocalDateTime.ofInstant(
|
||||
Instant.ofEpochSecond(mtime),
|
||||
ZoneId.systemDefault()));
|
||||
|
||||
// 获取权限
|
||||
fileInfo.setPermissions(entry.getAttrs().getPermissionsString());
|
||||
|
||||
files.add(fileInfo);
|
||||
}
|
||||
|
||||
return files;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("列出文件失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public String pwd(String sessionId) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
return channel.pwd();
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("获取当前路径失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void cd(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
channel.cd(path);
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("切换目录失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public FileInfo getFileInfo(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
SftpATTRS attrs = channel.stat(path);
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
|
||||
// 从路径提取文件名
|
||||
String fileName = path;
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash >= 0 && lastSlash < path.length() - 1) {
|
||||
fileName = path.substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
fileInfo.setName(fileName);
|
||||
fileInfo.setPath(path);
|
||||
fileInfo.setSize(attrs.getSize());
|
||||
fileInfo.setDirectory(attrs.isDir());
|
||||
|
||||
int mtime = attrs.getMTime();
|
||||
fileInfo.setModifiedTime(LocalDateTime.ofInstant(
|
||||
Instant.ofEpochSecond(mtime),
|
||||
ZoneId.systemDefault()));
|
||||
|
||||
fileInfo.setPermissions(attrs.getPermissionsString());
|
||||
|
||||
return fileInfo;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("获取文件信息失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件到SFTP
|
||||
public void uploadFile(String sessionId, InputStream inputStream,
|
||||
String remotePath, long fileSize) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
channel.put(inputStream, remotePath);
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
// 注意:不在此处关闭流,由调用者使用try-with-resources管理
|
||||
}
|
||||
|
||||
// 从SFTP下载文件
|
||||
public void downloadFile(String sessionId, String remotePath,
|
||||
OutputStream outputStream) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
channel.get(remotePath, outputStream);
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("下载失败: " + e.getMessage(), e);
|
||||
}
|
||||
// 注意:不在此处关闭流,由调用者使用try-with-resources管理
|
||||
}
|
||||
|
||||
// SFTP间传输
|
||||
public void transferBetweenSftp(String sourceSessionId, String sourcePath,
|
||||
String targetSessionId, String targetPath) throws Exception {
|
||||
// 创建临时文件
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
String tempFile = tempDir + File.separator + UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
// 从源SFTP下载到临时文件
|
||||
ChannelSftp sourceChannel = sessionManager.getSession(sourceSessionId);
|
||||
if (sourceChannel == null) {
|
||||
throw new Exception("源会话不存在或已断开");
|
||||
}
|
||||
|
||||
sourceChannel.get(sourcePath, tempFile);
|
||||
|
||||
// 上传临时文件到目标SFTP
|
||||
ChannelSftp targetChannel = sessionManager.getSession(targetSessionId);
|
||||
if (targetChannel == null) {
|
||||
throw new Exception("目标会话不存在或已断开");
|
||||
}
|
||||
|
||||
targetChannel.put(tempFile, targetPath);
|
||||
|
||||
} finally {
|
||||
// 删除临时文件
|
||||
File file = new File(tempFile);
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个文件或目录
|
||||
public boolean deleteFile(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
FileInfo fileInfo = getFileInfo(sessionId, path);
|
||||
if (fileInfo.isDirectory()) {
|
||||
deleteDirectoryRecursive(sessionId, path);
|
||||
} else {
|
||||
channel.rm(path);
|
||||
}
|
||||
return true;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("删除失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归删除SFTP目录
|
||||
private void deleteDirectoryRecursive(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
Vector<?> entries = channel.ls(path);
|
||||
|
||||
for (Object obj : entries) {
|
||||
ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) obj;
|
||||
String fileName = entry.getFilename();
|
||||
if (".".equals(fileName) || "..".equals(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String fullPath = path.endsWith("/") ?
|
||||
path + fileName :
|
||||
path + "/" + fileName;
|
||||
|
||||
if (entry.getAttrs().isDir()) {
|
||||
deleteDirectoryRecursive(sessionId, fullPath);
|
||||
} else {
|
||||
channel.rm(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
channel.rmdir(path);
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("删除目录失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建目录(单级)
|
||||
public boolean createDirectory(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
channel.stat(path);
|
||||
throw new Exception("目录已存在: " + path);
|
||||
} catch (SftpException e) {
|
||||
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
channel.mkdir(path);
|
||||
return true;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("创建目录失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建多级目录
|
||||
public boolean createDirectories(String sessionId, String path) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
channel.stat(path);
|
||||
throw new Exception("目录已存在: " + path);
|
||||
} catch (SftpException e) {
|
||||
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
createDirectoriesRecursive(channel, path);
|
||||
return true;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("创建目录失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归创建多级目录
|
||||
private void createDirectoriesRecursive(ChannelSftp channel, String path) throws SftpException {
|
||||
if (path == null || path.equals("/") || path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String parentPath = getSftpParentPath(path);
|
||||
if (parentPath != null && !parentPath.equals(path) && !parentPath.isEmpty()) {
|
||||
try {
|
||||
channel.stat(parentPath);
|
||||
} catch (SftpException e) {
|
||||
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
|
||||
createDirectoriesRecursive(channel, parentPath);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channel.mkdir(path);
|
||||
}
|
||||
|
||||
// 获取 SFTP 父路径
|
||||
private String getSftpParentPath(String path) {
|
||||
if (path == null || path.isEmpty()) return "/";
|
||||
String p = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
|
||||
int index = p.lastIndexOf("/");
|
||||
if (index <= 0) return "/";
|
||||
return p.substring(0, index);
|
||||
}
|
||||
|
||||
// 重命名文件
|
||||
public boolean renameFile(String sessionId, String oldPath, String newPath) throws Exception {
|
||||
ChannelSftp channel = sessionManager.getSession(sessionId);
|
||||
if (channel == null) {
|
||||
throw new Exception("会话不存在或已断开");
|
||||
}
|
||||
|
||||
try {
|
||||
channel.stat(oldPath);
|
||||
|
||||
try {
|
||||
channel.stat(newPath);
|
||||
throw new Exception("目标文件已存在: " + newPath);
|
||||
} catch (SftpException e) {
|
||||
// 目标不存在,可以重命名
|
||||
}
|
||||
|
||||
channel.rename(oldPath, newPath);
|
||||
return true;
|
||||
} catch (SftpException e) {
|
||||
throw new Exception("重命名失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
public BatchDeleteResult batchDelete(String sessionId, List<String> paths) {
|
||||
BatchDeleteResult result = new BatchDeleteResult();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
List<String> failedFiles = new ArrayList<>();
|
||||
|
||||
for (String path : paths) {
|
||||
try {
|
||||
deleteFile(sessionId, path);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
failedFiles.add(path + " - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
result.setSuccessCount(successCount);
|
||||
result.setFailCount(failCount);
|
||||
result.setFailedFiles(failedFiles);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user