From 05fd985addf33df9c1e56ac60507be4826eea8d2 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Fri, 18 Apr 2025 20:39:41 +0800 Subject: [PATCH] update --- packages/backend/src/services/sftp.service.ts | 37 ++++++++++++++++--- .../frontend/src/components/FileManager.vue | 2 +- .../src/composables/useFileUploader.ts | 6 +-- .../src/composables/useSftpActions.ts | 35 ++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/services/sftp.service.ts b/packages/backend/src/services/sftp.service.ts index 6ae7f0b..189adc0 100644 --- a/packages/backend/src/services/sftp.service.ts +++ b/packages/backend/src/services/sftp.service.ts @@ -641,11 +641,38 @@ export class SftpService { this.cancelUploadInternal(uploadId, 'Bytes written exceeded total size'); } else if (uploadState.bytesWritten === uploadState.totalSize) { - console.log(`[SFTP Upload ${uploadId}] All bytes (${uploadState.bytesWritten}) received for ${uploadState.remotePath}. Sending success and ending stream.`); - // Send success message IMMEDIATELY upon receiving the last expected byte - state.ws.send(JSON.stringify({ type: 'sftp:upload:success', payload: { uploadId, remotePath: uploadState.remotePath } })); - // Now end the stream. The 'close' event will handle cleanup. - uploadState.stream.end(); + console.log(`[SFTP Upload ${uploadId}] All bytes (${uploadState.bytesWritten}) received for ${uploadState.remotePath}. Fetching stats before sending success...`); + + // Get stats for the newly uploaded file before sending success + state.sftp!.lstat(uploadState.remotePath, (statErr, stats) => { + let newItemPayload: any = null; // Default to null payload + if (statErr) { + console.error(`[SFTP Upload ${uploadId}] lstat after upload ${uploadState.remotePath} failed:`, statErr); + // Still send success, but with null payload as item details are unavailable + } else { + newItemPayload = { + filename: uploadState.remotePath.substring(uploadState.remotePath.lastIndexOf('/') + 1), + longname: '', // lstat doesn't provide longname + attrs: { + size: stats.size, uid: stats.uid, gid: stats.gid, mode: stats.mode, + atime: stats.atime * 1000, mtime: stats.mtime * 1000, + isDirectory: stats.isDirectory(), isFile: stats.isFile(), isSymbolicLink: stats.isSymbolicLink(), + } + }; + console.log(`[SFTP Upload ${uploadId}] Sending upload success with new item details for ${uploadState.remotePath}`); + } + // Send success message with the newItem payload (or null if lstat failed) + state.ws.send(JSON.stringify({ type: 'sftp:upload:success', payload: newItemPayload, uploadId: uploadId, path: uploadState.remotePath })); // Include uploadId and path for frontend context + + // End the stream *after* lstat completes and success message is sent + uploadState.stream.end((endErr: Error | undefined) => { // Add type annotation + if (endErr) { + console.error(`[SFTP Upload ${uploadId}] Error ending write stream after success for ${uploadState.remotePath}:`, endErr); + } else { + console.log(`[SFTP Upload ${uploadId}] Write stream ended successfully after success for ${uploadState.remotePath}.`); + } + }); + }); } } catch (error: any) { diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index 3a77661..c0d6098 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -80,7 +80,7 @@ const { } = useFileUploader( currentPath, // 使用从 sftpManager 获取的 currentPath fileList, // 传递来自 sftpManager 的 fileList ref - () => loadDirectory(currentPath.value), // Refresh function uses manager's loadDirectory + // () => loadDirectory(currentPath.value), // 不再需要传递 refresh 函数 // props.sessionId, // 不再传递 sessionId // props.dbConnectionId // 不再传递 dbConnectionId props.wsDeps // 传递注入的 WebSocket 依赖项 diff --git a/packages/frontend/src/composables/useFileUploader.ts b/packages/frontend/src/composables/useFileUploader.ts index 5d603b2..d1a75a4 100644 --- a/packages/frontend/src/composables/useFileUploader.ts +++ b/packages/frontend/src/composables/useFileUploader.ts @@ -25,7 +25,7 @@ const joinPath = (base: string, name: string): string => { export function useFileUploader( currentPathRef: Ref, fileListRef: Readonly>, // 使用 Readonly 类型 - refreshDirectory: () => void, // 上传成功后刷新目录的回调函数 + // refreshDirectory: () => void, // 不再需要此回调 // sessionId: string, // 不再需要,因为 wsDeps 包含了会话上下文 // dbConnectionId: string, // 不再需要 wsDeps: WebSocketDependencies // 注入 WebSocket 依赖项 @@ -204,8 +204,8 @@ export function useFileUploader( upload.status = 'success'; upload.progress = 100; - // 使用回调刷新目录 - refreshDirectory(); + // 不再调用 refreshDirectory(),由 useSftpActions 处理列表更新 + // refreshDirectory(); // 延迟后从列表中移除 setTimeout(() => { diff --git a/packages/frontend/src/composables/useSftpActions.ts b/packages/frontend/src/composables/useSftpActions.ts index e573ca7..531bc60 100644 --- a/packages/frontend/src/composables/useSftpActions.ts +++ b/packages/frontend/src/composables/useSftpActions.ts @@ -561,6 +561,40 @@ 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 获取文件名 + + console.log(`[SFTP ${instanceSessionId}] 上传文件成功: ${filename ? joinPath(parentPath, filename) : '(未知文件名)'}`); // 改进日志 + + if (newItem && filename) { // 确保 newItem 和 filename 都存在 + const index = fileList.value.findIndex(item => item.filename === filename); + if (index !== -1) { + // 文件已存在 (覆盖上传),替换 + fileList.value.splice(index, 1, newItem); + console.log(`[SFTP ${instanceSessionId}] 直接更新被覆盖的文件信息: ${filename}`); + } else { + // 文件是新建的,插入 + let insertIndex = 0; + while (insertIndex < fileList.value.length && sortFiles(newItem, fileList.value[insertIndex]) > 0) { + insertIndex++; + } + fileList.value.splice(insertIndex, 0, newItem); + console.log(`[SFTP ${instanceSessionId}] 直接添加新上传的文件到列表: ${filename}`); + } + // 更新缓存 + directoryCache.set(currentPathRef.value, { list: [...fileList.value], timestamp: Date.now() }); + } else if (!newItem) { // 检查 newItem 是否为 null 或 undefined + // 如果后端未能提供更新信息,则刷新 + const filePathForLog = message.path || '(未知路径)'; // 尝试从 message 获取路径用于日志 + console.warn(`[SFTP ${instanceSessionId}] Upload success for ${filePathForLog} but no item details received. Reloading.`); + invalidateCache(currentPathRef.value); + loadDirectory(currentPathRef.value); + } + // 注意:移除了多余的 else 和 else if (!newItem) 块 + }; // <--- 确保右花括号在这里 const onActionError = (payload: MessagePayload, message: WebSocketMessage) => { // 类型断言,因为我们知道这些错误的 payload 是 string @@ -589,6 +623,7 @@ export function createSftpActionsManager( unregisterCallbacks.push(onMessage('sftp:rename:success', onRenameSuccess)); unregisterCallbacks.push(onMessage('sftp:chmod:success', onChmodSuccess)); unregisterCallbacks.push(onMessage('sftp:writefile:success', onWriteFileSuccess)); // 使用 onWriteFileSuccess + unregisterCallbacks.push(onMessage('sftp:upload:success', onUploadSuccess)); // *** 新增:监听上传成功 *** unregisterCallbacks.push(onMessage('sftp:mkdir:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:rmdir:error', onActionError)); unregisterCallbacks.push(onMessage('sftp:unlink:error', onActionError));