From 061e724368a5b87b06a5fe77964af3e8adb91bf6 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:00:25 +0800 Subject: [PATCH] update --- packages/backend/src/services/sftp.service.ts | 149 +++++++++++++++--- packages/backend/src/websocket.ts | 7 +- .../frontend/src/components/FileManager.vue | 4 +- .../file-manager/useFileManagerDragAndDrop.ts | 80 +++++++--- .../src/composables/useFileUploader.ts | 37 +++-- .../src/composables/useSftpActions.ts | 83 +++++++--- 6 files changed, 281 insertions(+), 79 deletions(-) diff --git a/packages/backend/src/services/sftp.service.ts b/packages/backend/src/services/sftp.service.ts index b9c9042..c2d0f12 100644 --- a/packages/backend/src/services/sftp.service.ts +++ b/packages/backend/src/services/sftp.service.ts @@ -52,6 +52,7 @@ interface ActiveUpload { bytesWritten: number; stream: WriteStream; sessionId: string; // Link back to the session for cleanup + relativePath?: string; // +++ 新增:存储相对路径 +++ } export class SftpService { @@ -730,35 +731,81 @@ export class SftpService { }); } - // +++ 新增:辅助方法 - 确保目录存在 (Promise wrapper) +++ - private ensureDirectoryExists(sftp: SFTPWrapper, dirPath: string): Promise { - return new Promise((resolve, reject) => { - sftp.stat(dirPath, (err: any, stats) => { // Cast err to any - if (err) { - // If error is 'No such file', create the directory - // Access err.code after casting to any - if (err.code === 'ENOENT' || (err.message && err.message.includes('No such file'))) { - sftp.mkdir(dirPath, (mkdirErr) => { + // +++ 修改:辅助方法 - 确保目录存在 (递归创建) +++ + private async ensureDirectoryExists(sftp: SFTPWrapper, dirPath: string): Promise { + // 规范化路径,移除尾部斜杠(如果存在) + const normalizedPath = dirPath.replace(/\/$/, ''); + if (!normalizedPath || normalizedPath === '/') { + return; // 根目录不需要创建 + } + + try { + // 1. 尝试直接 stat 目录 + await this.getStats(sftp, normalizedPath); + // console.log(`[SFTP Util] Directory already exists: ${normalizedPath}`); + return; // 目录已存在 + } catch (statError: any) { + // 2. 如果 stat 失败,检查是否是 "No such file" 错误 + if (statError.code === 'ENOENT' || (statError.message && statError.message.includes('No such file'))) { + // 目录不存在,尝试创建 + try { + // 3. 尝试递归创建 (ssh2 的 mkdir 支持非标准 recursive 属性) + // 注意:这可能不适用于所有 SFTP 服务器 + await new Promise((resolveMkdir, rejectMkdir) => { + // @ts-ignore - ssh2 types might not include 'recursive' in attributes + sftp.mkdir(normalizedPath, { recursive: true }, (mkdirErr) => { if (mkdirErr) { - reject(new Error(`创建目录失败 ${dirPath}: ${mkdirErr.message}`)); + // 如果递归创建失败,尝试逐级创建 + console.warn(`[SFTP Util] Recursive mkdir failed for ${normalizedPath}, falling back to iterative creation:`, mkdirErr); + rejectMkdir(mkdirErr); // Reject to trigger fallback } else { - console.log(`[SFTP Util] Created directory: ${dirPath}`); - resolve(); + console.log(`[SFTP Util] Recursively created directory: ${normalizedPath}`); + resolveMkdir(); } }); - } else { - // Other stat error - reject(new Error(`检查目录失败 ${dirPath}: ${err.message}`)); + }); + return; // 递归创建成功 + } catch (recursiveMkdirError) { + // 4. 递归创建失败,回退到逐级创建 + const parentDir = pathModule.dirname(normalizedPath).replace(/\\/g, '/'); + if (parentDir && parentDir !== '/' && parentDir !== '.') { + // 递归确保父目录存在 + await this.ensureDirectoryExists(sftp, parentDir); + } + // 创建当前目录 + try { + await new Promise((resolveMkdir, rejectMkdir) => { + sftp.mkdir(normalizedPath, (mkdirErr) => { + if (mkdirErr) { + // 如果逐级创建也失败,则抛出错误 + rejectMkdir(new Error(`创建目录失败 ${normalizedPath}: ${mkdirErr.message}`)); + } else { + console.log(`[SFTP Util] Iteratively created directory: ${normalizedPath}`); + resolveMkdir(); + } + }); + }); + } catch (iterativeMkdirError: any) { + console.error(`[SFTP Util] Iterative mkdir failed for ${normalizedPath}:`, iterativeMkdirError); + // 检查是否是因为目录已存在(可能由并发操作创建) + try { + const finalStats = await this.getStats(sftp, normalizedPath); + if (!finalStats.isDirectory()) { + throw new Error(`路径 ${normalizedPath} 已存在但不是目录`); + } + // 如果目录现在存在,则忽略错误 + console.log(`[SFTP Util] Directory ${normalizedPath} exists after iterative mkdir failure, likely created concurrently.`); + } catch (finalStatError) { + // 如果最终检查也失败,则抛出原始的逐级创建错误 + throw iterativeMkdirError; + } } - } else if (!stats.isDirectory()) { - // Path exists but is not a directory - reject(new Error(`路径 ${dirPath} 已存在但不是目录`)); - } else { - // Directory already exists - resolve(); } - }); - }); + } else { + // 其他 stat 错误 + throw new Error(`检查目录失败 ${normalizedPath}: ${statError.message}`); + } + } } // +++ 新增:辅助方法 - 列出目录内容 (Promise wrapper) +++ @@ -804,7 +851,8 @@ export class SftpService { // --- File Upload Methods --- /** Start a new file upload */ - startUpload(sessionId: string, uploadId: string, remotePath: string, totalSize: number): void { + // --- 修改:添加 relativePath 参数 --- + async startUpload(sessionId: string, uploadId: string, remotePath: string, totalSize: number, relativePath?: string): Promise { const state = this.clientStates.get(sessionId); if (!state || !state.sftp) { console.warn(`[SFTP Upload ${uploadId}] SFTP not ready for session ${sessionId}.`); @@ -820,6 +868,58 @@ export class SftpService { console.log(`[SFTP Upload ${uploadId}] Starting upload for ${remotePath} (${totalSize} bytes) in session ${sessionId}`); try { + // --- 新增:在创建流之前确保目录存在 --- + if (relativePath) { + const targetDirectory = pathModule.dirname(remotePath).replace(/\\/g, '/'); + console.log(`[SFTP Upload ${uploadId}] Ensuring directory exists: ${targetDirectory}`); + try { + // 确保 state.sftp 存在 + if (!state.sftp) throw new Error('SFTP session is not available.'); + await this.ensureDirectoryExists(state.sftp, targetDirectory); + console.log(`[SFTP Upload ${uploadId}] Directory ensured: ${targetDirectory}`); // +++ 增加成功日志 +++ + } catch (dirError: any) { + console.error(`[SFTP Upload ${uploadId}] Failed to create/ensure directory ${targetDirectory}:`, dirError); + state.ws.send(JSON.stringify({ type: 'sftp:upload:error', payload: { uploadId, message: `创建目录失败: ${dirError.message}` } })); + // 不再删除 activeUploads,因为可能还没有创建 + return; // Stop the upload process + } + } + // --- 结束新增 --- + + // --- 新增:预检查文件是否可写 --- + console.log(`[SFTP Upload ${uploadId}] Pre-checking writability for: ${remotePath}`); + try { + // 确保 state.sftp 存在 + if (!state.sftp) throw new Error('SFTP session is not available.'); + await new Promise((resolve, reject) => { + // 'w' flag: Open file for writing. The file is created (if it does not exist) or truncated (if it exists). + state.sftp!.open(remotePath, 'w', (openErr, handle) => { + if (openErr) { + console.error(`[SFTP Upload ${uploadId}] Pre-check failed (sftp.open 'w') for ${remotePath}:`, openErr); + return reject(openErr); // Reject if cannot open for writing + } + // Immediately close the handle, we just wanted to check writability + state.sftp!.close(handle, (closeErr) => { + if (closeErr) { + // Log warning but don't fail the pre-check if closing fails + console.warn(`[SFTP Upload ${uploadId}] Error closing handle during pre-check for ${remotePath}:`, closeErr); + } + console.log(`[SFTP Upload ${uploadId}] Pre-check successful for: ${remotePath}`); + resolve(); + }); + }); + }); + } catch (preCheckError: any) { + console.error(`[SFTP Upload ${uploadId}] Writability pre-check failed for ${remotePath}:`, preCheckError); + state.ws.send(JSON.stringify({ type: 'sftp:upload:error', payload: { uploadId, message: `文件不可写或创建失败: ${preCheckError.message}` } })); + return; // Stop if pre-check fails + } + // --- 结束新增 --- + + + console.log(`[SFTP Upload ${uploadId}] Creating write stream for: ${remotePath}`); + // 确保 state.sftp 存在 + if (!state.sftp) throw new Error('SFTP session is not available after pre-check.'); const stream = state.sftp.createWriteStream(remotePath); const uploadState: ActiveUpload = { remotePath, @@ -827,6 +927,7 @@ export class SftpService { bytesWritten: 0, stream, sessionId, + relativePath, // +++ 存储 relativePath +++ }; this.activeUploads.set(uploadId, uploadState); diff --git a/packages/backend/src/websocket.ts b/packages/backend/src/websocket.ts index fc2864e..f44e028 100644 --- a/packages/backend/src/websocket.ts +++ b/packages/backend/src/websocket.ts @@ -1085,8 +1085,11 @@ export const initializeWebSocket = async (server: http.Server, sessionParser: Re ws.send(JSON.stringify({ type: 'sftp:upload:error', payload: { uploadId: payload?.uploadId, message: '缺少 uploadId, remotePath 或 size' } })); return; } - - sftpService.startUpload(sessionId, payload.uploadId, payload.remotePath, payload.size); + // --- 修改:传递 relativePath 给 SftpService --- + const relativePath = payload?.relativePath; // 获取 relativePath + console.log(`WebSocket: SFTP Upload Start - Session: ${sessionId}, UploadID: ${payload.uploadId}, RemotePath: ${payload.remotePath}, Size: ${payload.size}, RelativePath: ${relativePath}`); + sftpService.startUpload(sessionId, payload.uploadId, payload.remotePath, payload.size, relativePath); // 传递 relativePath + // --- 结束修改 --- break; } case 'sftp:upload:chunk': { diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index 51e9962..f9c99bb 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -655,7 +655,9 @@ const handleFileSelected = (event: Event) => { const input = event.target as HTMLInputElement; // 恢复使用 props.wsDeps.isConnected if (!input.files || !props.wsDeps.isConnected.value) return; - Array.from(input.files).forEach(startFileUpload); // Use startFileUpload from useFileUploader + // --- 修正:使用匿名函数包装 startFileUpload 调用 --- + Array.from(input.files).forEach(file => startFileUpload(file)); // 只传递 file 参数 + // --- 结束修正 --- input.value = ''; }; diff --git a/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts b/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts index b716228..1f14e81 100644 --- a/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts +++ b/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts @@ -12,7 +12,7 @@ export interface UseFileManagerDragAndDropOptions { // 函数依赖 joinPath: (base: string, target: string) => string; // 路径拼接函数 - onFileUpload: (file: File) => void; // 触发文件上传的回调 + onFileUpload: (file: File, relativePath?: string) => void; // 修改:触发文件上传的回调,增加相对路径 onItemMove: (sourceItem: FileListItem, newFullPath: string) => void; // 触发文件/文件夹移动的回调 } @@ -146,40 +146,76 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti } }; + // --- 新增:递归遍历文件树的辅助函数 --- + const traverseFileTree = (item: FileSystemEntry, path = '') => { + path = path || ''; + if (item.isFile) { + // 文件处理 + (item as FileSystemFileEntry).file((file) => { + // 调用上传函数,传递文件和相对路径 + console.log(`[DragDrop] Uploading file: ${path}${file.name}`); + onFileUpload(file, path); // 传递相对路径 + }, (err) => { + console.error(`[DragDrop] Error getting file from entry: ${path}${item.name}`, err); + }); + } else if (item.isDirectory) { + // 目录处理 + const dirReader = (item as FileSystemDirectoryEntry).createReader(); + dirReader.readEntries((entries) => { + console.log(`[DragDrop] Traversing directory: ${path}${item.name}, found ${entries.length} entries.`); + // 递归遍历目录中的每个条目 + entries.forEach((entry) => { + traverseFileTree(entry, path + item.name + '/'); // 更新相对路径 + }); + }, (err) => { + console.error(`[DragDrop] Error reading directory entries: ${path}${item.name}`, err); + }); + } + }; + // --- 结束新增 --- + + const handleDrop = (event: DragEvent) => { const wasDraggingOver = isDraggingOver.value; - const currentDragTarget = dragOverTarget.value; + const currentDragTarget = dragOverTarget.value; // 拖放目标文件夹名称 isDraggingOver.value = false; dragOverTarget.value = null; stopAutoScroll(); - const files = event.dataTransfer?.files; - if (!files || files.length === 0 || !isConnected.value) { + // --- 修改:使用 DataTransferItemList 和 webkitGetAsEntry 处理拖放 --- + const items = event.dataTransfer?.items; + if (!items || items.length === 0 || !isConnected.value) { if (draggedItem.value) draggedItem.value = null; // 清理内部拖拽状态 return; } + // 检查放置目标是否有效 (由 handleDragOver 决定) // 对于外部文件,要么容器高亮 (wasDraggingOver),要么行高亮 (currentDragTarget) - if (!wasDraggingOver && !currentDragTarget) { - console.log(`[DragDrop] Drop ignored: Drop target was not valid according to handleDragOver.`); - return; + // 注意:拖放到子文件夹的功能暂时移除,所有拖放都上传到当前目录或根目录 + // 如果需要拖放到子文件夹,需要重新设计 targetFolderPath 的逻辑 + // if (!wasDraggingOver && !currentDragTarget) { + // console.log(`[DragDrop] Drop ignored: Drop target was not valid according to handleDragOver.`); + // return; + // } + + console.log(`[DragDrop] Drop event detected with ${items.length} items.`); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind === 'file') { + const entry = item.webkitGetAsEntry(); + if (entry) { + console.log(`[DragDrop] Processing entry: ${entry.name}, isFile: ${entry.isFile}, isDirectory: ${entry.isDirectory}`); + traverseFileTree(entry); // 开始遍历文件树,初始相对路径为空 + } else { + console.warn(`[DragDrop] Could not get entry for item ${i}`); + } + } else { + console.log(`[DragDrop] Skipping non-file item kind: ${item.kind}`); + } } + // --- 结束修改 --- - - const fileListArray = Array.from(files); - let targetFolderPath = currentPath.value; // 默认上传到当前目录 - - // 如果是放置在特定子文件夹行上 - if (currentDragTarget && currentDragTarget !== '..') { - targetFolderPath = joinPath(currentPath.value, currentDragTarget); - console.log(`[DragDrop] Dropped ${fileListArray.length} external files onto folder '${currentDragTarget}'. Uploading to: ${targetFolderPath}`); - } else { - console.log(`[DragDrop] Dropped ${fileListArray.length} external files onto current path '${currentPath.value}'.`); - } - - // 注意:原始代码中 startFileUpload 没有使用 targetFolderPath,这里暂时保持一致 - // 如果需要上传到子目录,需要修改 useFileUploader 或此处的调用方式 - fileListArray.forEach(onFileUpload); draggedItem.value = null; // 确保清理内部拖拽状态 }; diff --git a/packages/frontend/src/composables/useFileUploader.ts b/packages/frontend/src/composables/useFileUploader.ts index 7920ce6..2b0dfd1 100644 --- a/packages/frontend/src/composables/useFileUploader.ts +++ b/packages/frontend/src/composables/useFileUploader.ts @@ -123,7 +123,7 @@ export function useFileUploader( }; - const startFileUpload = (file: File) => { + const startFileUpload = (file: File, relativePath?: string) => { // 保持签名修改 if (!isConnected.value) { console.warn('[文件上传模块] 无法开始上传:WebSocket 未连接。'); // 可以选择向用户显示错误消息 @@ -131,16 +131,35 @@ export function useFileUploader( } const uploadId = generateUploadId(); - const remotePath = joinPath(currentPathRef.value, file.name); + // --- 修正:直接构建最终远程路径 --- + let finalRemotePath: string; + if (relativePath) { + // 确保 currentPathRef.value 结尾有斜杠 + const basePath = currentPathRef.value.endsWith('/') ? currentPathRef.value : `${currentPathRef.value}/`; + // 确保 relativePath 开头没有斜杠,末尾有斜杠 (如果非空) + let cleanRelativePath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; + // 移除末尾斜杠(如果有),因为文件名会加上 + cleanRelativePath = cleanRelativePath.endsWith('/') ? cleanRelativePath.slice(0, -1) : cleanRelativePath; + // 拼接路径,确保 cleanRelativePath 和 file.name 之间只有一个斜杠 + finalRemotePath = `${basePath}${cleanRelativePath ? cleanRelativePath + '/' : ''}${file.name}`; + } else { + finalRemotePath = joinPath(currentPathRef.value, file.name); // 对于非文件夹上传,保持原样 + } + // 规范化路径,移除多余的斜杠 e.g. /root//dir -> /root/dir + finalRemotePath = finalRemotePath.replace(/\/+/g, '/'); + console.log(`[文件上传模块] Calculated finalRemotePath: ${finalRemotePath} (current: ${currentPathRef.value}, relative: ${relativePath}, filename: ${file.name})`); // 添加日志 + // --- 结束修正 --- - // 使用传入的 fileListRef 检查是否覆盖 - // fileListRef.value 现在是 readonly FileListItem[] - if (fileListRef.value.some((item: FileListItem) => item.filename === file.name && !item.attrs.isDirectory)) { // 添加 item 类型注解 - if (!confirm(t('fileManager.prompts.confirmOverwrite', { name: file.name }))) { - console.log(`[文件上传模块] 用户取消了 ${file.name} 的上传`); + // --- 修正:检查覆盖逻辑需要使用 finalRemotePath 的 basename --- + const finalFilename = finalRemotePath.substring(finalRemotePath.lastIndexOf('/') + 1); + // 检查是否覆盖 *同名文件* (忽略目录) + if (fileListRef.value.some((item: FileListItem) => item.filename === finalFilename && !item.attrs.isDirectory)) { + if (!confirm(t('fileManager.prompts.confirmOverwrite', { name: finalFilename }))) { + console.log(`[文件上传模块] 用户取消了 ${finalFilename} 的上传`); return; // 用户取消覆盖 } } + // --- 结束修正 --- // 添加到响应式 uploads 字典 uploads[uploadId] = { @@ -151,10 +170,10 @@ export function useFileUploader( status: 'pending' // 初始状态 }; - console.log(`[文件上传模块] 开始上传 ${uploadId} 到 ${remotePath}`); + console.log(`[文件上传模块] 开始上传 ${uploadId} 到 ${finalRemotePath}`); // 使用 finalRemotePath sendMessage({ type: 'sftp:upload:start', - payload: { uploadId, remotePath, size: file.size } + payload: { uploadId, remotePath: finalRemotePath, size: file.size, relativePath: relativePath || undefined } // 发送修正后的 remotePath }); // 后端应该响应 sftp:upload:ready }; diff --git a/packages/frontend/src/composables/useSftpActions.ts b/packages/frontend/src/composables/useSftpActions.ts index 128394e..e1524a8 100644 --- a/packages/frontend/src/composables/useSftpActions.ts +++ b/packages/frontend/src/composables/useSftpActions.ts @@ -586,17 +586,35 @@ export function createSftpActionsManager( return false; }; - // *** 新增:辅助函数 - 向文件树添加或更新节点 *** + // *** 修改:辅助函数 - 向文件树添加或更新节点 (允许创建父节点占位符) *** const addOrUpdateNodeInTree = (parentPath: string, item: FileListItem): boolean => { - const parentNode = findNodeByPath(fileTree, parentPath); - if (parentNode && parentNode.childrenLoaded && parentNode.children) { // 确保父节点已加载子节点 - const newNode: FileTreeNode = { + // --- 修改:调用 findNodeByPath 时允许创建缺失的父节点 --- + const parentNode = findNodeByPath(fileTree, parentPath, true); + // --- 结束修改 --- + + // 如果父节点被成功找到或创建 + if (parentNode) { + // 如果父节点的 children 为 null (可能刚被创建为占位符),则初始化为空数组 + if (parentNode.children === null) { + parentNode.children = []; + // 注意:此时 childrenLoaded 应该仍然是 false,除非它是叶子节点 + // findNodeByPath 创建占位符时 childrenLoaded 设为 false + } + + // 确保 children 是一个数组再继续 + if (!Array.isArray(parentNode.children)) { + console.error(`[SFTP ${instanceSessionId}] Logic error: parentNode.children is not an array after findNodeByPath in addOrUpdateNodeInTree for path ${parentPath}`); + return false; // 无法继续 + } + + // --- 现有逻辑:添加或更新子节点 --- + const newNode: FileTreeNode = reactive({ // 确保新节点也是响应式的 filename: item.filename, longname: item.longname, attrs: item.attrs, children: item.attrs.isDirectory ? null : [], childrenLoaded: !item.attrs.isDirectory, - }; + }); const existingIndex = parentNode.children.findIndex(node => node.filename === item.filename); if (existingIndex !== -1) { @@ -612,16 +630,14 @@ export function createSftpActionsManager( parentNode.children.splice(insertIndex, 0, newNode); console.log(`[SFTP ${instanceSessionId}] 添加文件树节点: ${parentPath}/${item.filename}`); } - return true; - } else if (parentNode && !parentNode.childrenLoaded) { - // 父节点存在但子节点未加载,标记为需要重新加载 - parentNode.childrenLoaded = false; // 下次访问时会重新加载 - console.log(`[SFTP ${instanceSessionId}] 父节点 ${parentPath} 子节点未加载,标记为需要刷新`); - // 可以在这里触发一次 loadDirectory(parentPath) 如果需要立即更新 + // --- 结束现有逻辑 --- + return true; // 添加/更新成功 + } else { - console.warn(`[SFTP ${instanceSessionId}] 尝试向文件树 ${parentPath} 添加/更新节点 ${item.filename} 失败,父节点未找到或未加载`); + // 如果 findNodeByPath 即使在 createIfMissing=true 时也失败了,说明有更深层的问题 + console.error(`[SFTP ${instanceSessionId}] Failed to find or create parent node ${parentPath} in addOrUpdateNodeInTree for item ${item.filename}.`); + return false; // 添加/更新失败 } - return false; }; @@ -841,22 +857,47 @@ export function createSftpActionsManager( // *** 新增:处理上传成功 *** const onUploadSuccess = (payload: MessagePayload, message: WebSocketMessage) => { const newItem = payload as FileListItem | null; // 后端应发送 FileListItem 或 null - const parentPath = currentPathRef.value; // 上传总是发生在当前路径 - const filename = newItem?.filename; // 从 newItem 获取文件名 + const fullPath = message.path; // 后端现在应该在 message 中包含完整的上传路径 - console.log(`[SFTP ${instanceSessionId}] 上传文件成功: ${filename ? joinPath(parentPath, filename) : '(未知文件名)'}`); // 改进日志 + if (!fullPath) { + console.error(`[SFTP ${instanceSessionId}] Received upload success but message is missing 'path'. Payload:`, payload); + // 尝试从 newItem 获取文件名,但无法确定父路径,只能刷新当前目录 + const filename = newItem?.filename; + console.warn(`[SFTP ${instanceSessionId}] Upload success for ${filename || '(unknown file)'} but cannot determine parent path. Reloading current directory.`); + loadDirectory(currentPathRef.value); // Fallback to reloading current dir + return; + } - // *** 修改:直接修改文件树 *** + // --- 修正:从完整路径推断父路径和文件名 --- + const parentPath = fullPath.substring(0, fullPath.lastIndexOf('/')) || '/'; + const filename = fullPath.substring(fullPath.lastIndexOf('/') + 1); + // --- 结束修正 --- + + console.log(`[SFTP ${instanceSessionId}] 上传文件成功: ${fullPath}`); + + // *** 修改:使用推断出的 parentPath 更新文件树 *** if (newItem) { + // 确保 newItem 的 filename 与从路径中提取的一致 + if (newItem.filename !== filename) { + console.warn(`[SFTP ${instanceSessionId}] Upload success: filename mismatch between message.path ('${filename}') and payload.filename ('${newItem.filename}'). Using filename from path.`); + // 可以选择信任哪个,这里信任从路径提取的 + newItem.filename = filename; + } addOrUpdateNodeInTree(parentPath, newItem); } else { - // 如果后端未能提供更新信息,标记父节点需要重新加载 + // 如果后端未能提供更新信息,标记推断出的父节点需要重新加载 const parentNode = findNodeByPath(fileTree, parentPath); if (parentNode) { parentNode.childrenLoaded = false; - console.warn(`[SFTP ${instanceSessionId}] Upload success for ${message.path || filename} but no item details received. Marking parent ${parentPath} for reload.`); - // 上传总是在当前目录,所以直接触发刷新 - loadDirectory(currentPathRef.value); + console.warn(`[SFTP ${instanceSessionId}] Upload success for ${fullPath} but no item details received. Marking parent ${parentPath} for reload.`); + // 如果上传发生在当前目录或其子目录,触发当前目录刷新可能有用 + if (parentPath === currentPathRef.value || parentPath.startsWith(currentPathRef.value + '/')) { + loadDirectory(currentPathRef.value); + } + } else { + console.warn(`[SFTP ${instanceSessionId}] Upload success for ${fullPath}, no item details, and parent node ${parentPath} not found in tree.`); + // 可能需要刷新根目录或当前目录 + loadDirectory(currentPathRef.value); } } };