feat: 增加对文件管理器中软链接的处理

This commit is contained in:
Baobhan Sith
2025-05-23 11:50:41 +08:00
parent d23a06c371
commit d8f0524b7c
2 changed files with 188 additions and 40 deletions
+54 -4
View File
@@ -692,11 +692,61 @@ export class SftpService {
state.sftp.realpath(path, (err, absPath) => {
if (err) {
console.error(`[SFTP ${sessionId}] realpath ${path} failed (ID: ${requestId}):`, err);
state.ws.send(JSON.stringify({ type: 'sftp:realpath:error', path: path, payload: `获取绝对路径失败: ${err.message}`, requestId: requestId }));
state.ws.send(JSON.stringify({ type: 'sftp:realpath:error', path: path, payload: { requestedPath: path, error: `获取绝对路径失败: ${err.message}` }, requestId: requestId }));
} else {
console.log(`[SFTP ${sessionId}] realpath ${path} -> ${absPath} success (ID: ${requestId})`);
// 在 payload 中同时发送请求的路径和绝对路径
state.ws.send(JSON.stringify({ type: 'sftp:realpath:success', path: path, payload: { requestedPath: path, absolutePath: absPath }, requestId: requestId }));
console.log(`[SFTP ${sessionId}] realpath ${path} -> ${absPath} success (ID: ${requestId}). Fetching target type...`);
// 再次检查 state 和 state.sftp 是否仍然有效,因为回调是异步的
const currentState = this.clientStates.get(sessionId);
if (!currentState || !currentState.sftp) {
console.warn(`[SFTP ${sessionId}] SFTP session for ${absPath} became invalid before stat call (ID: ${requestId}).`);
// 即使 SFTP 会话失效,也尝试发送已解析的路径,但标记错误
state.ws.send(JSON.stringify({
type: 'sftp:realpath:error',
path: path, // 原始请求路径
payload: {
requestedPath: path,
absolutePath: absPath,
error: 'SFTP 会话在获取目标类型前已失效'
},
requestId: requestId
}));
return;
}
// 对 absPath 执行 stat 操作以获取其真实类型
currentState.sftp.stat(absPath, (statErr, stats) => { // 使用 sftp.stat()
if (statErr) {
console.error(`[SFTP ${sessionId}] stat on realpath target ${absPath} failed (ID: ${requestId}):`, statErr);
// 如果 stat 失败,发送带有错误信息的 realpath:error,但仍包含已解析的路径
state.ws.send(JSON.stringify({
type: 'sftp:realpath:error',
path: path, // 原始请求路径
payload: {
requestedPath: path,
absolutePath: absPath, // 仍然发送已解析的路径
error: `获取目标类型失败: ${statErr.message}`
},
requestId: requestId
}));
} else {
let targetType: 'file' | 'directory' | 'unknown' = 'unknown';
if (stats.isFile()) {
targetType = 'file';
} else if (stats.isDirectory()) {
targetType = 'directory';
}
console.log(`[SFTP ${sessionId}] Target type for ${absPath} is ${targetType} (ID: ${requestId})`);
state.ws.send(JSON.stringify({
type: 'sftp:realpath:success',
path: path, // 原始请求路径
payload: {
requestedPath: path,
absolutePath: absPath,
targetType: targetType // 新增字段
},
requestId: requestId
}));
}
});
}
});
} catch (error: any) {
+134 -36
View File
@@ -235,50 +235,148 @@ const handleSort = (key: keyof FileListItem | 'type' | 'size' | 'mtime') => {
// --- (使 Composable) ---
// ( Selection )
const handleItemAction = (item: FileListItem) => {
// currentSftpManager
if (!currentSftpManager.value) return;
if (!currentSftpManager.value) return;
if (item.attrs.isDirectory) {
// 使 currentSftpManager.value.isLoading
if (currentSftpManager.value.isLoading.value) {
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Ignoring directory click, already loading...`);
return;
}
const newPath = item.filename === '..'
// 使 currentSftpManager.value currentPath joinPath
? currentSftpManager.value.currentPath.value.substring(0, currentSftpManager.value.currentPath.value.lastIndexOf('/')) || '/'
: currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename);
// 使 currentSftpManager.value.loadDirectory
currentSftpManager.value.loadDirectory(newPath);
} else if (item.attrs.isFile) {
//
const itemPath = currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename);
if (item.attrs.isSymbolicLink) {
if (currentSftpManager.value.isLoading.value) {
return;
}
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Symbolic link clicked: ${itemPath}. Attempting to resolve with sftp:realpath...`);
const { sendMessage: wsSend, onMessage: wsOnMessage } = props.wsDeps;
const requestId = generateRequestId();
const handleResolvedPath = (realPath: string, targetType: 'file' | 'directory' | 'unknown', originalLinkItem: FileListItem) => {
if (!currentSftpManager.value) return;
if (targetType === 'directory') {
currentSftpManager.value.loadDirectory(realPath);
} else if (targetType === 'file') {
const targetFilename = realPath.substring(realPath.lastIndexOf('/') + 1) || originalLinkItem.filename; // Get filename from realPath
const fileInfo: FileInfo = { name: targetFilename, fullPath: realPath };
// Preserve mobile multi-select behavior for the original link item
if (props.isMobile && isMultiSelectMode.value) {
if (selectedItems.value.has(item.filename)) {
selectedItems.value.delete(item.filename);
} else {
selectedItems.value.add(item.filename);
}
return;
if (selectedItems.value.has(originalLinkItem.filename)) {
selectedItems.value.delete(originalLinkItem.filename);
} else {
selectedItems.value.add(originalLinkItem.filename);
}
return;
}
// 使 currentSftpManager.value currentPath joinPath
const filePath = currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename);
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
if (settingsStore.showPopupFileEditorBoolean) {
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering popup for: ${filePath}`);
fileEditorStore.triggerPopup(filePath, props.sessionId); // Popup sessionId
fileEditorStore.triggerPopup(realPath, props.sessionId);
}
if (shareFileEditorTabsBoolean.value) {
fileEditorStore.openFile(realPath, props.sessionId, props.instanceId);
} else {
sessionStore.openFileInSession(props.sessionId, fileInfo);
}
} else { // targetType is 'unknown' or not provided as expected
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Symlink target '${realPath}' has an unknown type from server ('${targetType}'). Defaulting to open as file.`);
// Fallback: attempt to open as file, or display an error
const targetFilename = realPath.substring(realPath.lastIndexOf('/') + 1) || originalLinkItem.filename;
const fileInfo: FileInfo = { name: targetFilename, fullPath: realPath };
if (settingsStore.showPopupFileEditorBoolean) {
fileEditorStore.triggerPopup(realPath, props.sessionId);
}
if (shareFileEditorTabsBoolean.value) {
fileEditorStore.openFile(realPath, props.sessionId, props.instanceId);
} else {
sessionStore.openFileInSession(props.sessionId, fileInfo);
}
}
};
let unregisterSuccess: (() => void) | undefined;
let unregisterError: (() => void) | undefined;
let timeoutId: NodeJS.Timeout | number | undefined;
const cleanupListeners = () => {
unregisterSuccess?.();
unregisterError?.();
if (timeoutId) clearTimeout(timeoutId as any);
timeoutId = undefined;
};
unregisterSuccess = wsOnMessage('sftp:realpath:success', (payload: any, message: WebSocketMessage) => {
if (message.requestId === requestId && payload.requestedPath === itemPath) {
cleanupListeners();
if (!currentSftpManager.value) return;
// payload absolutePath targetType
const absolutePath = payload.absolutePath;
const targetType = payload.targetType as ('file' | 'directory' | 'unknown'); //
if (!absolutePath) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] sftp:realpath:success for ${itemPath} missing absolutePath. Payload:`, payload);
alert(`Failed to resolve symbolic link "${item.filename}": Server did not return a valid path.`);
return;
}
if (!targetType) {
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] sftp:realpath:success for ${itemPath} missing targetType. Defaulting to 'file'. Payload:`, payload);
}
if (shareFileEditorTabsBoolean.value) {
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in shared mode (store handles loading): ${filePath}`);
// instanceId openFile
fileEditorStore.openFile(filePath, props.sessionId, props.instanceId);
} else {
// sessionStore instanceId
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in independent mode (session store handles loading): ${filePath}`);
sessionStore.openFileInSession(props.sessionId, fileInfo); // Independent mode sessionId
}
handleResolvedPath(absolutePath, targetType || 'unknown', item);
}
});
unregisterError = wsOnMessage('sftp:realpath:error', (payload: any, message: WebSocketMessage) => {
if (message.requestId === requestId && payload?.requestedPath === itemPath) {
cleanupListeners();
// payload.error
// payload.absolutePath stat
const serverErrorMsg = payload.error || 'Unknown error resolving symlink target type';
const resolvedPathInfo = payload.absolutePath ? ` (Resolved path: ${payload.absolutePath})` : '';
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to get realpath or target type for symlink '${itemPath}': ${serverErrorMsg}${resolvedPathInfo}`);
alert(`Failed to resolve symbolic link "${item.filename}": ${serverErrorMsg}.${resolvedPathInfo} Please ensure the target exists and you have permissions.`);
}
});
timeoutId = setTimeout(() => {
cleanupListeners();
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Timeout getting realpath for symlink '${itemPath}' (ID: ${requestId}).`);
alert(`Timeout resolving symbolic link "${item.filename}".`);
}, 10000); // 10
wsSend({ type: 'sftp:realpath', requestId: requestId, payload: { path: itemPath } });
return; // Handled by async callbacks
}
if (item.attrs.isDirectory) {
if (currentSftpManager.value.isLoading.value) {
return;
}
const newPath = item.filename === '..'
? currentSftpManager.value.currentPath.value.substring(0, currentSftpManager.value.currentPath.value.lastIndexOf('/')) || '/'
: currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename);
currentSftpManager.value.loadDirectory(newPath);
} else if (item.attrs.isFile) {
// This block now only handles regular files, as symlinks are handled above.
if (props.isMobile && isMultiSelectMode.value) {
if (selectedItems.value.has(item.filename)) {
selectedItems.value.delete(item.filename);
} else {
selectedItems.value.add(item.filename);
}
return;
}
const filePath = itemPath; // itemPath is already calculated
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
if (settingsStore.showPopupFileEditorBoolean) {
fileEditorStore.triggerPopup(filePath, props.sessionId);
}
if (shareFileEditorTabsBoolean.value) {
fileEditorStore.openFile(filePath, props.sessionId, props.instanceId);
} else {
sessionStore.openFileInSession(props.sessionId, fileInfo);
}
}
};
// ()