Enhance file transfer capabilities by introducing support for multiple source paths and recursive directory transfers in FileController and LocalFileService. Updated TransferRequest to accommodate new fields, improved error handling, and refined UI interactions in app.js for better user experience during file operations.

This commit is contained in:
liu
2026-02-03 22:41:56 +08:00
parent 765c6f0021
commit 72641eb7d7
7 changed files with 395 additions and 98 deletions

View File

@@ -24,6 +24,7 @@ import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/files")
@@ -176,61 +177,201 @@ public class FileController {
// 服务器间传输
@PostMapping("/transfer")
public ApiResponse<Void> transferFiles(@RequestBody TransferRequest request) {
String sourceSessionId = request.getSourceSessionId();
String targetSessionId = request.getTargetSessionId();
String targetPath = request.getTargetPath();
String prefix;
if ("local".equals(sourceSessionId) && !"local".equals(targetSessionId)) {
prefix = "上传到服务器失败";
} else if (!"local".equals(sourceSessionId) && "local".equals(targetSessionId)) {
prefix = "下载到本地失败";
} else if (!"local".equals(sourceSessionId) && !"local".equals(targetSessionId)) {
prefix = "服务器间传输失败";
} else {
prefix = "传输失败";
}
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();
List<String> sourcePaths = request.getSourcePaths();
if (sourcePaths == null || sourcePaths.isEmpty()) {
// 兼容旧字段:仅当 sourcePaths 为空时才使用单个 sourcePath
if (request.getSourcePath() == null || request.getSourcePath().isEmpty()) {
return ApiResponse.error("源路径不能为空");
}
sourcePaths = java.util.Collections.singletonList(request.getSourcePath());
}
// 构建目标路径
String finalTargetPath = targetPath.endsWith("/") ?
targetPath + fileName :
targetPath + "/" + fileName;
boolean recursive = Boolean.TRUE.equals(request.getRecursive());
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);
if (targetPath == null || targetPath.isEmpty()) {
return ApiResponse.error("目标路径不能为空");
}
return ApiResponse.success("传输成功", null);
int successCount = 0;
int failCount = 0;
StringBuilder failMessages = new StringBuilder();
for (String sourcePath : sourcePaths) {
if (sourcePath == null || sourcePath.isEmpty()) {
failCount++;
failMessages.append("源路径为空; ");
continue;
}
boolean isDirectory;
String fileName;
if ("local".equals(sourceSessionId)) {
File file = new File(sourcePath);
if (!file.exists()) {
failCount++;
failMessages.append(sourcePath).append(" 不存在; ");
continue;
}
isDirectory = file.isDirectory();
fileName = file.getName();
} else {
FileInfo fileInfo = sftpService.getFileInfo(sourceSessionId, sourcePath);
isDirectory = fileInfo.isDirectory();
fileName = fileInfo.getName();
}
// 构建目标路径(目标目录下的文件/目录名称与源名称保持一致)
String finalTargetPath = targetPath.endsWith("/") ?
targetPath + fileName :
targetPath + "/" + fileName;
try {
if ("local".equals(sourceSessionId) && "local".equals(targetSessionId)) {
// 本地到本地
if (isDirectory) {
if (!recursive) {
throw new Exception("目录传输需要开启递归recursive=true");
}
copyLocalDirectory(sourcePath, finalTargetPath);
} else {
Files.copy(new File(sourcePath).toPath(), new File(finalTargetPath).toPath());
}
} else if ("local".equals(sourceSessionId)) {
// 本地到SFTP上传到服务器
if (isDirectory) {
if (!recursive) {
throw new Exception("目录传输需要开启递归recursive=true");
}
localFileService.uploadDirectoryToSftp(sourcePath, targetSessionId, finalTargetPath, sftpService);
} else {
localFileService.uploadToSftp(sourcePath, targetSessionId, finalTargetPath, sftpService);
}
} else if ("local".equals(targetSessionId)) {
// SFTP到本地从服务器下载
if (isDirectory) {
if (!recursive) {
throw new Exception("目录传输需要开启递归recursive=true");
}
localFileService.downloadDirectoryFromSftp(sourceSessionId, sourcePath, finalTargetPath, sftpService);
} else {
localFileService.downloadFromSftp(sourceSessionId, sourcePath, finalTargetPath, sftpService);
}
} else {
// SFTP到SFTP
if (isDirectory) {
if (!recursive) {
throw new Exception("目录传输需要开启递归recursive=true");
}
// 通过本地临时目录实现 SFTP 之间的目录传输
String tempRoot = System.getProperty("java.io.tmpdir") +
File.separator + "sftp-manager-" + UUID.randomUUID();
File tempDir = new File(tempRoot);
try {
localFileService.downloadDirectoryFromSftp(sourceSessionId, sourcePath, tempRoot, sftpService);
localFileService.uploadDirectoryToSftp(tempRoot, targetSessionId, finalTargetPath, sftpService);
} finally {
deleteLocalDirectory(tempDir);
}
} else {
sftpService.transferBetweenSftp(sourceSessionId, sourcePath,
targetSessionId, finalTargetPath);
}
}
successCount++;
} catch (Exception e) {
failCount++;
String msg = e.getMessage() != null ? e.getMessage() : "未知错误";
failMessages.append(sourcePath).append(" - ").append(msg).append("; ");
}
}
if (failCount == 0) {
return ApiResponse.success("传输成功", null);
} else if (successCount == 0) {
String msg = failMessages.length() > 0 ? failMessages.toString() : "全部传输失败";
return ApiResponse.error(prefix + ": " + msg);
} else {
String msg = "部分文件传输失败,成功 " + successCount + " 个,失败 " + failCount + " 个。详情: " + failMessages;
return ApiResponse.error(prefix + ": " + msg);
}
} catch (Exception e) {
String msg = e.getMessage() != null ? e.getMessage() : "未知错误";
String src = request.getSourceSessionId();
String tgt = request.getTargetSessionId();
String prefix;
if ("local".equals(src) && !"local".equals(tgt)) {
prefix = "上传到服务器失败";
} else if (!"local".equals(src) && "local".equals(tgt)) {
prefix = "下载到本地失败";
} else if (!"local".equals(src) && !"local".equals(tgt)) {
prefix = "服务器间传输失败";
} else {
prefix = "传输失败";
}
return ApiResponse.error(prefix + ": " + msg);
}
}
/**
* 递归复制本地目录
*/
private void copyLocalDirectory(String sourceDirPath, String targetDirPath) throws Exception {
File sourceDir = new File(sourceDirPath);
if (!sourceDir.exists() || !sourceDir.isDirectory()) {
throw new Exception("本地目录不存在: " + sourceDirPath);
}
File targetDir = new File(targetDirPath);
if (!targetDir.exists() && !targetDir.mkdirs()) {
throw new Exception("创建目标目录失败: " + targetDirPath);
}
File[] files = sourceDir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
File targetChild = new File(targetDir, file.getName());
if (file.isDirectory()) {
copyLocalDirectory(file.getAbsolutePath(), targetChild.getAbsolutePath());
} else {
Files.copy(file.toPath(), targetChild.toPath());
}
}
}
/**
* 递归删除本地目录(用于清理临时目录)
*/
private void deleteLocalDirectory(File directory) {
if (directory == null || !directory.exists()) {
return;
}
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteLocalDirectory(file);
} else {
try {
Files.deleteIfExists(file.toPath());
} catch (Exception ignored) {
}
}
}
}
try {
Files.deleteIfExists(directory.toPath());
} catch (Exception ignored) {
}
}
// 删除单个文件
@DeleteMapping("/delete")
public ApiResponse<Void> deleteFile(@RequestParam String sessionId,

View File

@@ -2,10 +2,21 @@ package com.sftp.manager.dto;
import lombok.Data;
import java.util.List;
@Data
public class TransferRequest {
private String sourceSessionId; // 源会话ID
private String sourcePath; // 源文件路径
/**
* 源路径列表(支持一次传输多个文件/目录)
* 兼容旧字段:当 sourcePaths 为空且 sourcePath 不为空时,后端会自动将 sourcePath 转为单元素列表
*/
private List<String> sourcePaths;
private String targetSessionId; // 目标会话ID
private String targetPath; // 目标路径
/**
* 是否递归处理目录(包含子目录和文件)
*/
private Boolean recursive;
}

View File

@@ -9,12 +9,14 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileVisitOption;
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.EnumSet;
import java.util.List;
@Service
@@ -110,6 +112,61 @@ public class LocalFileService {
}
}
/**
* 递归上传本地目录到 SFTP保持目录结构
*
* @param localDirPath 本地目录路径
* @param sessionId SFTP 会话 ID
* @param remoteDirPath 目标 SFTP 目录路径(对应 localDirPath 本身)
*/
public void uploadDirectoryToSftp(String localDirPath, String sessionId,
String remoteDirPath, SftpService sftpService) throws Exception {
File root = new File(localDirPath);
if (!root.exists() || !root.isDirectory()) {
throw new Exception("本地目录不存在: " + localDirPath);
}
Path rootPath = root.toPath();
try {
// 先确保根目录存在
sftpService.ensureDirectories(sessionId, remoteDirPath);
Files.walk(rootPath, FileVisitOption.FOLLOW_LINKS).forEach(path -> {
try {
if (Files.isDirectory(path)) {
if (path.equals(rootPath)) {
// 根目录已处理
return;
}
String relative = rootPath.relativize(path).toString().replace(File.separatorChar, '/');
String targetDir = remoteDirPath.endsWith("/") ?
remoteDirPath + relative :
remoteDirPath + "/" + relative;
sftpService.ensureDirectories(sessionId, targetDir);
} else {
String relative = rootPath.relativize(path).toString().replace(File.separatorChar, '/');
String targetFilePath = remoteDirPath.endsWith("/") ?
remoteDirPath + relative :
remoteDirPath + "/" + relative;
String parent = targetFilePath.contains("/") ?
targetFilePath.substring(0, targetFilePath.lastIndexOf('/')) :
"/";
sftpService.ensureDirectories(sessionId, parent);
uploadToSftp(path.toString(), sessionId, targetFilePath, sftpService);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException e) {
if (e.getCause() instanceof Exception) {
throw (Exception) e.getCause();
}
throw e;
}
}
// 从SFTP下载到本地
public void downloadFromSftp(String sessionId, String remotePath,
String localPath, SftpService sftpService) throws Exception {
@@ -126,6 +183,47 @@ public class LocalFileService {
}
}
/**
* 递归从 SFTP 下载目录到本地,保持目录结构
*
* @param sessionId SFTP 会话 ID
* @param remoteDirPath 远程目录路径
* @param localDirPath 本地目标目录路径(对应 remoteDirPath 本身)
*/
public void downloadDirectoryFromSftp(String sessionId, String remoteDirPath,
String localDirPath, SftpService sftpService) throws Exception {
File root = new File(localDirPath);
if (!root.exists() && !root.mkdirs()) {
throw new Exception("创建本地目录失败: " + localDirPath);
}
downloadDirectoryFromSftpRecursive(sessionId, remoteDirPath, root, sftpService);
}
private void downloadDirectoryFromSftpRecursive(String sessionId, String remoteDirPath,
File localDir, SftpService sftpService) throws Exception {
if (!localDir.exists() && !localDir.mkdirs()) {
throw new Exception("创建本地目录失败: " + localDir.getPath());
}
List<FileInfo> remoteFiles = sftpService.listFiles(sessionId, remoteDirPath, true);
if (remoteFiles == null) {
return;
}
for (FileInfo fileInfo : remoteFiles) {
boolean isDir = fileInfo.isDirectory();
String name = fileInfo.getName();
String childRemotePath = fileInfo.getPath();
File childLocal = new File(localDir, name);
if (isDir) {
downloadDirectoryFromSftpRecursive(sessionId, childRemotePath, childLocal, sftpService);
} else {
downloadFromSftp(sessionId, childRemotePath, childLocal.getAbsolutePath(), sftpService);
}
}
}
// 删除单个文件或目录
public boolean deleteFile(String path) throws Exception {
File file = new File(path);

View File

@@ -304,6 +304,36 @@ public class SftpService {
}
}
/**
* 确保多级目录存在(若不存在则创建,已存在时不报错)
*/
public void ensureDirectories(String sessionId, String path) throws Exception {
ChannelSftp channel = sessionManager.getSession(sessionId);
if (channel == null) {
throw new Exception("会话不存在或已断开");
}
if (path == null || path.isEmpty() || "/".equals(path)) {
return;
}
try {
channel.stat(path);
// 目录已存在,直接返回
return;
} catch (SftpException e) {
if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
throw new Exception("检查目录失败: " + e.getMessage(), e);
}
}
try {
createDirectoriesRecursive(channel, path);
} 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()) {