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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/files") @RequestMapping("/api/files")
@@ -176,58 +177,198 @@ public class FileController {
// 服务器间传输 // 服务器间传输
@PostMapping("/transfer") @PostMapping("/transfer")
public ApiResponse<Void> transferFiles(@RequestBody TransferRequest request) { public ApiResponse<Void> transferFiles(@RequestBody TransferRequest request) {
try {
String sourceSessionId = request.getSourceSessionId(); String sourceSessionId = request.getSourceSessionId();
String sourcePath = request.getSourcePath();
String targetSessionId = request.getTargetSessionId(); String targetSessionId = request.getTargetSessionId();
String targetPath = request.getTargetPath(); 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) {
String msg = e.getMessage() != null ? e.getMessage() : "未知错误";
String src = request.getSourceSessionId();
String tgt = request.getTargetSessionId();
String prefix; String prefix;
if ("local".equals(src) && !"local".equals(tgt)) { if ("local".equals(sourceSessionId) && !"local".equals(targetSessionId)) {
prefix = "上传到服务器失败"; prefix = "上传到服务器失败";
} else if (!"local".equals(src) && "local".equals(tgt)) { } else if (!"local".equals(sourceSessionId) && "local".equals(targetSessionId)) {
prefix = "下载到本地失败"; prefix = "下载到本地失败";
} else if (!"local".equals(src) && !"local".equals(tgt)) { } else if (!"local".equals(sourceSessionId) && !"local".equals(targetSessionId)) {
prefix = "服务器间传输失败"; prefix = "服务器间传输失败";
} else { } else {
prefix = "传输失败"; prefix = "传输失败";
} }
try {
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());
}
boolean recursive = Boolean.TRUE.equals(request.getRecursive());
if (targetPath == null || targetPath.isEmpty()) {
return ApiResponse.error("目标路径不能为空");
}
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); 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() : "未知错误";
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) {
} }
} }

View File

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

View File

@@ -9,12 +9,14 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.FileVisitOption;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List; import java.util.List;
@Service @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下载到本地 // 从SFTP下载到本地
public void downloadFromSftp(String sessionId, String remotePath, public void downloadFromSftp(String sessionId, String remotePath,
String localPath, SftpService sftpService) throws Exception { 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 { public boolean deleteFile(String path) throws Exception {
File file = new File(path); 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 { private void createDirectoriesRecursive(ChannelSftp channel, String path) throws SftpException {
if (path == null || path.equals("/") || path.isEmpty()) { if (path == null || path.equals("/") || path.isEmpty()) {

View File

@@ -171,7 +171,8 @@
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
onerror="this.onerror=null;var s=document.createElement('script');s.src='https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js';document.body.appendChild(s);"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>

View File

@@ -273,7 +273,12 @@ function getParentPath(path) {
function updateSelectedFiles(panelId) { function updateSelectedFiles(panelId) {
const selected = $(`#${panelId}-file-list .file-item.selected`); const selected = $(`#${panelId}-file-list .file-item.selected`);
panelState[panelId].selectedFiles = selected.map(function() { panelState[panelId].selectedFiles = selected.map(function() {
return $(this).data('path'); return {
path: $(this).data('path'),
isDirectory: $(this).data('is-dir') === true ||
$(this).data('is-dir') === 'true' ||
$(this).data('is-dir') === 'True'
};
}).get(); }).get();
} }
@@ -371,9 +376,10 @@ function handleFileDrop(data, targetPanelId) {
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ data: JSON.stringify({
sourceSessionId: sourceSessionId, sourceSessionId: sourceSessionId,
sourcePath: sourcePath, sourcePaths: [sourcePath],
targetSessionId: targetSessionId, targetSessionId: targetSessionId,
targetPath: targetPath targetPath: targetPath,
recursive: true
}), }),
success: function(response) { success: function(response) {
showTransferProgress(false); showTransferProgress(false);
@@ -623,8 +629,17 @@ function getFileNameFromPath(path) {
} }
// ========== 连接管理模块03========== // ========== 连接管理模块03==========
function ensureBootstrap() {
if (typeof bootstrap === 'undefined') {
alert('Bootstrap 未加载,请检查网络连接或暂时关闭广告拦截器后刷新页面');
return false;
}
return true;
}
function showConnectionDialog() { function showConnectionDialog() {
loadConnectionList(); loadConnectionList();
if (!ensureBootstrap()) return;
new bootstrap.Modal(document.getElementById('connectionModal')).show(); new bootstrap.Modal(document.getElementById('connectionModal')).show();
} }
@@ -758,6 +773,7 @@ function updatePanelStateWithConnection(sessionId, conn) {
function showAddConnectionDialog() { function showAddConnectionDialog() {
document.getElementById('connection-form').reset(); document.getElementById('connection-form').reset();
if (!ensureBootstrap()) return;
new bootstrap.Modal(document.getElementById('addConnectionModal')).show(); new bootstrap.Modal(document.getElementById('addConnectionModal')).show();
} }
@@ -780,7 +796,9 @@ function saveConnection() {
data: JSON.stringify(data), data: JSON.stringify(data),
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
if (typeof bootstrap !== 'undefined') {
bootstrap.Modal.getInstance(document.getElementById('addConnectionModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('addConnectionModal')).hide();
}
loadConnectionList(); loadConnectionList();
} else { } else {
alert('保存失败: ' + (response.message || '未知错误')); alert('保存失败: ' + (response.message || '未知错误'));
@@ -824,7 +842,9 @@ function connectToServer(connId) {
updateConnectionSelect('right'); updateConnectionSelect('right');
updatePanelStateWithConnection(sessionId, conn); updatePanelStateWithConnection(sessionId, conn);
loadConnectionList(); loadConnectionList();
if (typeof bootstrap !== 'undefined') {
bootstrap.Modal.getInstance(document.getElementById('connectionModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('connectionModal')).hide();
}
updateStatus('已连接到 ' + (conn.name || conn.host)); updateStatus('已连接到 ' + (conn.name || conn.host));
} else { } else {
alert('连接失败: ' + (res.message || '未知错误')); alert('连接失败: ' + (res.message || '未知错误'));
@@ -1056,7 +1076,7 @@ function downloadFile(sessionId, path) {
// 下载选中的文件 // 下载选中的文件
function downloadFiles() { function downloadFiles() {
const panelId = getSourcePanelId(); const panelId = getSourcePanelId();
const selectedFiles = panelState[panelId].selectedFiles; const selectedFiles = panelState[panelId].selectedFiles || [];
const sessionId = panelState[panelId].sessionId; const sessionId = panelState[panelId].sessionId;
if (selectedFiles.length === 0) { if (selectedFiles.length === 0) {
@@ -1064,8 +1084,14 @@ function downloadFiles() {
return; return;
} }
selectedFiles.forEach(function(path) { selectedFiles.forEach(function(item) {
downloadFile(sessionId, path); if (!item || !item.path) return;
if (item.isDirectory) {
if (!confirm('当前选中包含文件夹,下载时将递归包含其所有子目录和文件,可能耗时较长,是否继续?')) {
return;
}
}
downloadFile(sessionId, item.path);
}); });
} }
@@ -1074,7 +1100,7 @@ function doTransfer(sourcePanelId, targetPanelId) {
const sourceSessionId = panelState[sourcePanelId].sessionId; const sourceSessionId = panelState[sourcePanelId].sessionId;
const targetSessionId = panelState[targetPanelId].sessionId; const targetSessionId = panelState[targetPanelId].sessionId;
const targetPath = panelState[targetPanelId].currentPath; const targetPath = panelState[targetPanelId].currentPath;
const selectedFiles = panelState[sourcePanelId].selectedFiles; const selectedFiles = panelState[sourcePanelId].selectedFiles || [];
if (selectedFiles.length === 0) { if (selectedFiles.length === 0) {
alert('请在' + (sourcePanelId === 'left' ? '左侧' : '右侧') + '面板选择要传输的文件'); alert('请在' + (sourcePanelId === 'left' ? '左侧' : '右侧') + '面板选择要传输的文件');
@@ -1093,54 +1119,43 @@ function doTransfer(sourcePanelId, targetPanelId) {
showTransferCountProgress(0, total, ''); showTransferCountProgress(0, total, '');
updateTransferProgress(0, '传输中 (0/' + total + ')', true); // 后端无流式进度,使用动画条 updateTransferProgress(0, '传输中 (0/' + total + ')', true); // 后端无流式进度,使用动画条
selectedFiles.forEach(function(sourcePath) { const sourcePaths = selectedFiles
.filter(function(item) { return item && item.path; })
.map(function(item) { return item.path; });
const hasDirectory = selectedFiles.some(function(item) { return item && item.isDirectory; });
const recursive = hasDirectory; // 若包含目录,则自动递归
$.ajax({ $.ajax({
url: API_BASE + 'api/files/transfer', url: API_BASE + 'api/files/transfer',
method: 'POST', method: 'POST',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({ data: JSON.stringify({
sourceSessionId: sourceSessionId, sourceSessionId: sourceSessionId,
sourcePath: sourcePath, sourcePaths: sourcePaths,
targetSessionId: targetSessionId, targetSessionId: targetSessionId,
targetPath: targetPath targetPath: targetPath,
recursive: recursive
}), }),
success: function(response) { success: function(response) {
if (response.success) {
completed++;
} else {
failed++;
alert('传输失败: ' + (response.message || '未知错误'));
}
const done = completed + failed;
showTransferCountProgress(done, total, getFileNameFromPath(sourcePath));
updateTransferProgress(total > 0 ? Math.round((done / total) * 100) : 0, '传输中 (' + done + '/' + total + ')', false);
if (done === total) {
showTransferProgress(false); showTransferProgress(false);
if (failed === 0) { if (response.success) {
updateStatus('所有文件传输成功'); updateStatus('传输成功');
} else { } else {
updateStatus('传输完成:成功 ' + completed + ',失败 ' + failed); alert('传输失败: ' + (response.message || '未知错误'));
updateStatus('传输失败');
} }
loadFiles(targetPanelId); loadFiles(targetPanelId);
}
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
failed++; showTransferProgress(false);
const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error; const errMsg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : error;
alert('传输失败: ' + errMsg); alert('传输失败: ' + errMsg);
const done = completed + failed; updateStatus('传输失败');
showTransferCountProgress(done, total, getFileNameFromPath(sourcePath));
updateTransferProgress(total > 0 ? Math.round((done / total) * 100) : 0, '传输中 (' + done + '/' + total + ')', false);
if (done === total) {
showTransferProgress(false);
updateStatus('传输完成:成功 ' + completed + ',失败 ' + failed);
loadFiles(targetPanelId);
} }
}
});
}); });
updateStatus('正在传输 ' + total + ' 个文件...'); updateStatus('正在传输 ' + total + ' 个项目...');
} }
// 传输到右侧:左侧面板选中的文件 -> 右侧面板 // 传输到右侧:左侧面板选中的文件 -> 右侧面板

View File

@@ -158,7 +158,8 @@
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
onerror="this.onerror=null;var s=document.createElement('script');s.src='https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js';document.body.appendChild(s);"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>