This commit is contained in:
Baobhan Sith
2025-04-15 23:16:00 +08:00
parent 6ee18743ad
commit 1a6ea421e6
16 changed files with 1435 additions and 915 deletions
@@ -1,43 +1,68 @@
import { ref, readonly, type Ref, onUnmounted } from 'vue';
import { useWebSocketConnection } from './useWebSocketConnection'; // 只导入 hook
import { useI18n } from 'vue-i18n';
// 确保从类型文件导入所有需要的类型
import type { FileListItem, FileAttributes, EditorFileContent } from '../types/sftp.types';
import type { WebSocketMessage, MessagePayload } from '../types/websocket.types'; // 从类型文件导入
import { ref, readonly, type Ref, type ComputedRef } from 'vue'; // Removed onUnmounted, added ComputedRef
import type { FileListItem, EditorFileContent } from '../types/sftp.types';
import type { WebSocketMessage, MessagePayload, MessageHandler } from '../types/websocket.types'; // 从类型文件导入
// --- 接口定义 (已移至 sftp.types.ts) ---
/**
* @interface WebSocketDependencies
* @description Defines the necessary functions and state required from a WebSocket manager instance.
*/
export interface WebSocketDependencies {
sendMessage: (message: WebSocketMessage) => void;
onMessage: (type: string, handler: MessageHandler) => () => void;
isConnected: ComputedRef<boolean>;
isSftpReady: Readonly<Ref<boolean>>;
}
// Helper function (Copied from FileManager.vue)
const generateRequestId = (): string => {
return `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
};
// Helper function
const generateRequestId = (): string => `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
// Helper function (Copied from FileManager.vue)
// Helper function
const joinPath = (base: string, name: string): string => {
if (base === '/') return `/${name}`;
// Handle cases where base might end with '/' already
if (base.endsWith('/')) return `${base}${name}`;
return `${base}/${name}`;
return base.endsWith('/') ? `${base}${name}` : `${base}/${name}`;
};
// Helper function (Copied from FileManager.vue)
// Helper function
const sortFiles = (a: FileListItem, b: FileListItem): number => {
if (a.attrs.isDirectory && !b.attrs.isDirectory) return -1;
if (!a.attrs.isDirectory && b.attrs.isDirectory) return 1;
return a.filename.localeCompare(b.filename);
};
export function useSftpActions(currentPathRef: Ref<string>) {
const { t } = useI18n();
// Import isSftpReady along with other needed functions/state
const { sendMessage, onMessage, isConnected, isSftpReady } = useWebSocketConnection();
/**
* 创建并管理单个 SFTP 会话的操作。
* 每个实例对应一个会话 (Session) 并依赖于一个 WebSocket 管理器实例。
*
* @param {string} sessionId - 此 SFTP 管理器关联的会话 ID (用于日志记录)。
* @param {Ref<string>} currentPathRef - 一个外部 ref,用于跟踪和更新当前目录路径。
* @param {WebSocketDependencies} wsDeps - 从对应的 WebSocket 管理器实例注入的依赖项。
* @param {Function} t - i18n 翻译函数,从父组件传入
* @returns 一个包含状态、方法和清理函数的 SFTP 操作管理器对象。
*/
export function createSftpActionsManager(
sessionId: string,
currentPathRef: Ref<string>,
wsDeps: WebSocketDependencies,
t: Function
) {
const { sendMessage, onMessage, isConnected, isSftpReady } = wsDeps; // 使用注入的依赖
const fileList = ref<FileListItem[]>([]);
const isLoading = ref<boolean>(false);
const error = ref<string | null>(null);
const instanceSessionId = sessionId; // 保存会话 ID 用于日志
// Function to clear the error state
// 用于存储注销函数的数组
const unregisterCallbacks: (() => void)[] = [];
// 清理函数,用于注销所有消息处理器
const cleanup = () => {
console.log(`[SFTP ${instanceSessionId}] Cleaning up message handlers.`);
unregisterCallbacks.forEach(cb => cb());
unregisterCallbacks.length = 0; // 清空数组
};
// 清除错误状态的函数
const clearSftpError = () => {
error.value = null;
};
@@ -45,42 +70,37 @@ export function useSftpActions(currentPathRef: Ref<string>) {
// --- Action Methods ---
const loadDirectory = (path: string) => {
// Check if SFTP is ready first
if (!isSftpReady.value) {
error.value = t('fileManager.errors.sftpNotReady'); // Use a specific error message
error.value = t('fileManager.errors.sftpNotReady');
isLoading.value = false;
fileList.value = []; // Clear list if not ready
console.warn(`[useSftpActions] Attempted to load directory ${path} but SFTP is not ready.`);
fileList.value = [];
console.warn(`[SFTP ${instanceSessionId}] Attempted to load directory ${path} but SFTP is not ready.`);
return;
}
// Original isConnected check might still be relevant as a fallback, but isSftpReady implies isConnected
// if (!isConnected.value) { ... } // Can likely be removed if isSftpReady logic is robust
console.log(`[useSftpActions] Loading directory: ${path}`);
console.log(`[SFTP ${instanceSessionId}] Loading directory: ${path}`);
isLoading.value = true;
error.value = null;
currentPathRef.value = path; // Update the external ref passed in
currentPathRef.value = path; // 更新外部 ref
const requestId = generateRequestId();
sendMessage({ type: 'sftp:readdir', requestId: requestId, payload: { path } });
// Response handled by onSftpReaddirSuccess/Error
};
const createDirectory = (newDirName: string) => {
if (!isSftpReady.value) {
error.value = t('fileManager.errors.sftpNotReady');
console.warn(`[useSftpActions] Attempted to create directory ${newDirName} but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to create directory ${newDirName} but SFTP is not ready.`);
return;
}
const newFolderPath = joinPath(currentPathRef.value, newDirName);
const requestId = generateRequestId();
sendMessage({ type: 'sftp:mkdir', requestId: requestId, payload: { path: newFolderPath } });
// Response handled by onSftpMkdirSuccess/Error
};
const createFile = (newFileName: string) => {
if (!isSftpReady.value) {
error.value = t('fileManager.errors.sftpNotReady');
console.warn(`[useSftpActions] Attempted to create file ${newFileName} but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to create file ${newFileName} but SFTP is not ready.`);
return;
}
const newFilePath = joinPath(currentPathRef.value, newFileName);
@@ -88,15 +108,14 @@ export function useSftpActions(currentPathRef: Ref<string>) {
sendMessage({
type: 'sftp:writefile',
requestId: requestId,
payload: { path: newFilePath, content: '', encoding: 'utf8' } // Create by writing empty content
payload: { path: newFilePath, content: '', encoding: 'utf8' }
});
// Response handled by onSftpWriteFileSuccess/Error (will trigger refresh)
};
const deleteItems = (items: FileListItem[]) => {
if (!isSftpReady.value) {
error.value = t('fileManager.errors.sftpNotReady');
console.warn(`[useSftpActions] Attempted to delete items but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to delete items but SFTP is not ready.`);
return;
}
if (items.length === 0) return;
@@ -106,13 +125,12 @@ export function useSftpActions(currentPathRef: Ref<string>) {
const requestId = generateRequestId();
sendMessage({ type: actionType, requestId: requestId, payload: { path: targetPath } });
});
// Responses handled by onSftpRmdirSuccess/Error, onSftpUnlinkSuccess/Error (will trigger refresh)
};
const renameItem = (item: FileListItem, newName: string) => {
if (!isSftpReady.value) {
error.value = t('fileManager.errors.sftpNotReady');
console.warn(`[useSftpActions] Attempted to rename item ${item.filename} but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to rename item ${item.filename} but SFTP is not ready.`);
return;
}
if (!newName || item.filename === newName) return;
@@ -120,82 +138,96 @@ export function useSftpActions(currentPathRef: Ref<string>) {
const newPath = joinPath(currentPathRef.value, newName);
const requestId = generateRequestId();
sendMessage({ type: 'sftp:rename', requestId: requestId, payload: { oldPath, newPath } });
// Response handled by onSftpRenameSuccess/Error (will trigger refresh)
};
const changePermissions = (item: FileListItem, mode: number) => {
if (!isSftpReady.value) {
error.value = t('fileManager.errors.sftpNotReady');
console.warn(`[useSftpActions] Attempted to change permissions for ${item.filename} but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to change permissions for ${item.filename} but SFTP is not ready.`);
return;
}
const targetPath = joinPath(currentPathRef.value, item.filename);
const requestId = generateRequestId();
sendMessage({ type: 'sftp:chmod', requestId: requestId, payload: { path: targetPath, mode: mode } });
// Response handled by onSftpChmodSuccess/Error (will trigger refresh)
};
// 注意: readFile 和 writeFile 的核心逻辑将由 useFileEditor 管理,
// 但 useSftpActions 可以提供基础的发送/接收机制(如果其他地方需要),
// 或者 useFileEditor 可以直接调用 sendMessage。暂时保留这些方法在这里。
const readFile = (path: string): Promise<EditorFileContent> => { // 使用导入的 EditorFileContent 类型
// readFile 和 writeFile 仍然返回 Promise,并在内部处理自己的消息监听器注销
const readFile = (path: string): Promise<EditorFileContent> => {
return new Promise((resolve, reject) => {
if (!isSftpReady.value) {
console.warn(`[useSftpActions] Attempted to read file ${path} but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to read file ${path} but SFTP is not ready.`);
return reject(new Error(t('fileManager.errors.sftpNotReady')));
}
const requestId = generateRequestId();
let unregisterSuccess: (() => void) | null = null;
let unregisterError: (() => void) | null = null;
const unregisterSuccess = onMessage('sftp:readfile:success', (payload, message) => {
const timeoutId = setTimeout(() => {
unregisterSuccess?.();
unregisterError?.();
reject(new Error(t('fileManager.errors.readFileTimeout')));
}, 20000); // 20 秒超时
unregisterSuccess = onMessage('sftp:readfile:success', (payload: MessagePayload, message: WebSocketMessage) => {
// 确保 payload 是期望的类型
const successPayload = payload as { content: string; encoding: 'utf8' | 'base64' };
if (message.requestId === requestId && message.path === path) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
resolve({ content: payload.content, encoding: payload.encoding });
resolve({ content: successPayload.content, encoding: successPayload.encoding });
}
});
const unregisterError = onMessage('sftp:readfile:error', (payload, message) => {
unregisterError = onMessage('sftp:readfile:error', (payload: MessagePayload, message: WebSocketMessage) => {
// 确保 payload 是期望的类型 (string)
const errorPayload = payload as string;
if (message.requestId === requestId && message.path === path) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
reject(new Error(payload || 'Failed to read file'));
reject(new Error(errorPayload || 'Failed to read file'));
}
});
sendMessage({ type: 'sftp:readfile', requestId: requestId, payload: { path } });
// Timeout for the request
setTimeout(() => {
unregisterSuccess?.();
unregisterError?.();
reject(new Error(t('fileManager.errors.readFileTimeout')));
}, 20000); // 20 second timeout
});
};
const writeFile = (path: string, content: string): Promise<void> => {
return new Promise((resolve, reject) => {
if (!isSftpReady.value) {
console.warn(`[useSftpActions] Attempted to write file ${path} but SFTP is not ready.`);
console.warn(`[SFTP ${instanceSessionId}] Attempted to write file ${path} but SFTP is not ready.`);
return reject(new Error(t('fileManager.errors.sftpNotReady')));
}
const requestId = generateRequestId();
const encoding: 'utf8' | 'base64' = 'utf8'; // Assuming always sending utf8
const encoding: 'utf8' | 'base64' = 'utf8'; // 假设总是 utf8
let unregisterSuccess: (() => void) | null = null;
let unregisterError: (() => void) | null = null;
const unregisterSuccess = onMessage('sftp:writefile:success', (payload, message) => {
const timeoutId = setTimeout(() => {
unregisterSuccess?.();
unregisterError?.();
reject(new Error(t('fileManager.errors.saveTimeout')));
}, 20000); // 20 秒超时
unregisterSuccess = onMessage('sftp:writefile:success', (payload: MessagePayload, message: WebSocketMessage) => {
if (message.requestId === requestId && message.path === path) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
resolve();
}
});
const unregisterError = onMessage('sftp:writefile:error', (payload, message) => {
unregisterError = onMessage('sftp:writefile:error', (payload: MessagePayload, message: WebSocketMessage) => {
// 确保 payload 是期望的类型 (string)
const errorPayload = payload as string;
if (message.requestId === requestId && message.path === path) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
reject(new Error(payload || 'Failed to write file'));
reject(new Error(errorPayload || 'Failed to write file'));
}
});
@@ -204,112 +236,80 @@ export function useSftpActions(currentPathRef: Ref<string>) {
requestId: requestId,
payload: { path, content, encoding }
});
// Timeout for the request
setTimeout(() => {
unregisterSuccess?.();
unregisterError?.();
reject(new Error(t('fileManager.errors.saveTimeout')));
}, 20000); // 20 second timeout
});
};
// --- Message Handlers ---
const onSftpReaddirSuccess = (payload: FileListItem[], message: WebSocketMessage) => {
// Only update if the path matches the current path this composable instance is tracking
const onSftpReaddirSuccess = (payload: MessagePayload, message: WebSocketMessage) => {
// 类型断言,因为我们知道 readdir:success 的 payload 是 FileListItem[]
const fileListPayload = payload as FileListItem[];
if (message.path === currentPathRef.value) {
console.log(`[useSftpActions] Received file list for ${message.path}`);
fileList.value = payload.sort(sortFiles);
console.log(`[SFTP ${instanceSessionId}] Received file list for ${message.path}`);
fileList.value = fileListPayload.sort(sortFiles);
isLoading.value = false;
error.value = null;
} else {
console.log(`[useSftpActions] Ignoring readdir success for ${message.path} (current: ${currentPathRef.value})`);
console.log(`[SFTP ${instanceSessionId}] Ignoring readdir success for ${message.path} (current: ${currentPathRef.value})`);
}
};
const onSftpReaddirError = (payload: string, message: WebSocketMessage) => {
const onSftpReaddirError = (payload: MessagePayload, message: WebSocketMessage) => {
// 类型断言,因为我们知道 readdir:error 的 payload 是 string
const errorPayload = payload as string;
if (message.path === currentPathRef.value) {
console.error(`[useSftpActions] Error loading directory ${message.path}:`, payload);
error.value = payload; // Set the error message
console.error(`[SFTP ${instanceSessionId}] Error loading directory ${message.path}:`, errorPayload);
error.value = errorPayload;
isLoading.value = false;
// Do NOT clear fileList.value here, keep the previous list visible
}
};
// Generic handler for actions that should trigger a refresh on success
const onActionSuccessRefresh = (payload: MessagePayload, message: WebSocketMessage) => {
// Simplify: Always refresh the current directory on any relevant success action.
// This avoids potential issues with path comparison logic.
console.log(`[useSftpActions] Action ${message.type} successful. Refreshing current directory: ${currentPathRef.value}`);
loadDirectory(currentPathRef.value); // Refresh the current directory
error.value = null; // Clear previous errors on success
console.log(`[SFTP ${instanceSessionId}] Action ${message.type} successful. Refreshing current directory: ${currentPathRef.value}`);
loadDirectory(currentPathRef.value);
error.value = null;
};
// Generic handler for action errors
const onActionError = (payload: string, message: WebSocketMessage) => {
console.error(`[useSftpActions] Action ${message.type} failed:`, payload);
// Display a generic error or use specific messages based on type
const onActionError = (payload: MessagePayload, message: WebSocketMessage) => {
// 类型断言,因为我们知道这些错误的 payload 是 string
const errorPayload = payload as string;
console.error(`[SFTP ${instanceSessionId}] Action ${message.type} failed:`, errorPayload);
const actionTypeMap: Record<string, string> = {
'sftp:mkdir:error': t('fileManager.errors.createFolderFailed'),
'sftp:rmdir:error': t('fileManager.errors.deleteFailed'),
'sftp:unlink:error': t('fileManager.errors.deleteFailed'),
'sftp:rename:error': t('fileManager.errors.renameFailed'),
'sftp:chmod:error': t('fileManager.errors.chmodFailed'),
'sftp:writefile:error': t('fileManager.errors.saveFailed'), // Added writefile error
'sftp:writefile:error': t('fileManager.errors.saveFailed'),
};
const prefix = actionTypeMap[message.type] || t('fileManager.errors.generic');
error.value = `${prefix}: ${payload}`;
// Optionally stop loading indicator if one was active for this action
error.value = `${prefix}: ${errorPayload}`;
};
// --- Register Handlers ---
const unregisterReaddirSuccess = onMessage('sftp:readdir:success', onSftpReaddirSuccess);
const unregisterReaddirError = onMessage('sftp:readdir:error', onSftpReaddirError);
// --- Register Handlers & Store Unregister Callbacks ---
unregisterCallbacks.push(onMessage('sftp:readdir:success', onSftpReaddirSuccess));
unregisterCallbacks.push(onMessage('sftp:readdir:error', onSftpReaddirError));
unregisterCallbacks.push(onMessage('sftp:mkdir:success', onActionSuccessRefresh));
unregisterCallbacks.push(onMessage('sftp:rmdir:success', onActionSuccessRefresh));
unregisterCallbacks.push(onMessage('sftp:unlink:success', onActionSuccessRefresh));
unregisterCallbacks.push(onMessage('sftp:rename:success', onActionSuccessRefresh));
unregisterCallbacks.push(onMessage('sftp:chmod:success', onActionSuccessRefresh));
unregisterCallbacks.push(onMessage('sftp:writefile:success', onActionSuccessRefresh));
unregisterCallbacks.push(onMessage('sftp:mkdir:error', onActionError));
unregisterCallbacks.push(onMessage('sftp:rmdir:error', onActionError));
unregisterCallbacks.push(onMessage('sftp:unlink:error', onActionError));
unregisterCallbacks.push(onMessage('sftp:rename:error', onActionError));
unregisterCallbacks.push(onMessage('sftp:chmod:error', onActionError));
unregisterCallbacks.push(onMessage('sftp:writefile:error', onActionError));
// Register generic handlers for actions that trigger refresh on success
const unregisterMkdirSuccess = onMessage('sftp:mkdir:success', onActionSuccessRefresh);
const unregisterRmdirSuccess = onMessage('sftp:rmdir:success', onActionSuccessRefresh);
const unregisterUnlinkSuccess = onMessage('sftp:unlink:success', onActionSuccessRefresh);
const unregisterRenameSuccess = onMessage('sftp:rename:success', onActionSuccessRefresh);
const unregisterChmodSuccess = onMessage('sftp:chmod:success', onActionSuccessRefresh);
const unregisterWritefileSuccess = onMessage('sftp:writefile:success', onActionSuccessRefresh); // Refresh on successful write too
// Register generic error handlers
const unregisterMkdirError = onMessage('sftp:mkdir:error', onActionError);
const unregisterRmdirError = onMessage('sftp:rmdir:error', onActionError);
const unregisterUnlinkError = onMessage('sftp:unlink:error', onActionError);
const unregisterRenameError = onMessage('sftp:rename:error', onActionError);
const unregisterChmodError = onMessage('sftp:chmod:error', onActionError);
const unregisterWritefileError = onMessage('sftp:writefile:error', onActionError); // Handle writefile error display
// Unregister handlers when the composable's scope is destroyed
onUnmounted(() => {
console.log('[useSftpActions] Unmounting and unregistering handlers.');
unregisterReaddirSuccess?.();
unregisterReaddirError?.();
unregisterMkdirSuccess?.();
unregisterRmdirSuccess?.();
unregisterUnlinkSuccess?.();
unregisterRenameSuccess?.();
unregisterChmodSuccess?.();
unregisterWritefileSuccess?.();
unregisterMkdirError?.();
unregisterRmdirError?.();
unregisterUnlinkError?.();
unregisterRenameError?.();
unregisterChmodError?.();
unregisterWritefileError?.();
// Note: readFile/writeFile promise handlers are unregistered within the promise logic
});
// 移除 onUnmounted 块
return {
// State
fileList: readonly(fileList),
isLoading: readonly(isLoading),
error: readonly(error),
// currentPath: readonly(currentPath), // Path is managed via the passed ref
// Methods
loadDirectory,
@@ -318,9 +318,12 @@ export function useSftpActions(currentPathRef: Ref<string>) {
deleteItems,
renameItem,
changePermissions,
readFile, // Expose if needed by editor composable
writeFile, // Expose if needed by editor composable
joinPath, // Expose helper if needed externally
clearSftpError, // Expose the clear error function
readFile,
writeFile,
joinPath, // 暴露辅助函数
clearSftpError,
// Cleanup function
cleanup,
};
}