diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 20c8f816..1476dd28 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -329,6 +329,71 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { }); }); +app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { + const sessionId = req.query.sessionId as string; + const sshConn = sshSessions[sessionId]; + const linkPath = decodeURIComponent(req.query.path as string); + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!linkPath) { + return res.status(400).json({ error: "Link path is required" }); + } + + sshConn.lastActive = Date.now(); + + const escapedPath = linkPath.replace(/'/g, "'\"'\"'"); + const command = `stat -L -c "%F" '${escapedPath}' && readlink -f '${escapedPath}'`; + + sshConn.client.exec(command, (err, stream) => { + if (err) { + fileLogger.error("SSH identifySymlink error:", err); + return res.status(500).json({ error: err.message }); + } + + let data = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + data += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + if (code !== 0) { + fileLogger.error( + `SSH identifySymlink command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + return res.status(500).json({ error: `Command failed: ${errorData}` }); + } + + const [fileType, target] = data.trim().split("\n"); + + res.json({ + path: linkPath, + target: target, + type: fileType.toLowerCase().includes("directory") ? "directory" : "file" + }); + }); + + stream.on("error", (streamErr) => { + fileLogger.error("SSH identifySymlink stream error:", streamErr); + if (!res.headersSent) { + res.status(500).json({ error: `Stream error: ${streamErr.message}` }); + } + }); + }); +}); + app.get("/ssh/file_manager/ssh/readFile", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx index 0cd317ea..f9e82c10 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx @@ -8,6 +8,7 @@ import React, { import { Folder, File, + FileSymlink, ArrowUp, Pin, MoreVertical, @@ -29,6 +30,7 @@ import { removeFileManagerPinned, getSSHStatus, connectSSH, + identifySSHSymlink, } from "@/ui/main-axios.ts"; import type { SSHHost } from "../../../types/index.js"; @@ -339,7 +341,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( try { await renameSSHItem(sshSessionId, item.path, newName.trim()); toast.success( - `${item.type === "directory" ? t("common.folder") : t("common.file")} ${t("common.renamedSuccessfully")}`, + `${item.type === "directory" ? t("common.folder") : item.type === "link" ? t("common.link") : t("common.file")} ${t("common.renamedSuccessfully")}`, ); setRenamingItem(null); if (onOperationComplete) { @@ -375,6 +377,74 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( onPathChange?.(newPath); }; + // Handle symlink resolution + const handleSymlinkClick = async (item: any) => { + if (!host) return; + + try { + // Extract just the symlink path (before the " -> " if present) + const symlinkPath = item.path.includes(" -> ") + ? item.path.split(" -> ")[0] + : item.path; + + let currentSessionId = sshSessionId; + + // Check SSH connection status and reconnect if needed + if (currentSessionId) { + try { + const status = await getSSHStatus(currentSessionId); + if (!status.connected) { + const newSessionId = await connectToSSH(host); + if (newSessionId) { + setSshSessionId(newSessionId); + currentSessionId = newSessionId; + } else { + throw new Error(t("fileManager.failedToReconnectSSH")); + } + } + } catch (sessionErr) { + const newSessionId = await connectToSSH(host); + if (newSessionId) { + setSshSessionId(newSessionId); + currentSessionId = newSessionId; + } else { + throw sessionErr; + } + } + } else { + // No session ID, try to connect + const newSessionId = await connectToSSH(host); + if (newSessionId) { + setSshSessionId(newSessionId); + currentSessionId = newSessionId; + } else { + throw new Error(t("fileManager.failedToConnectSSH")); + } + } + + const symlinkInfo = await identifySSHSymlink(currentSessionId, symlinkPath); + + if (symlinkInfo.type === "directory") { + // If symlink points to a directory, navigate to it + handlePathChange(symlinkInfo.target); + } else if (symlinkInfo.type === "file") { + // If symlink points to a file, open it as a file + onOpenFile({ + name: item.name, + path: symlinkInfo.target, // Use the target path, not the symlink path + isSSH: item.isSSH, + sshSessionId: currentSessionId, + }); + } + } catch (error: any) { + toast.error( + error?.response?.data?.error || + error?.message || + t("fileManager.failedToResolveSymlink"), + ); + } + }; + return (
@@ -456,6 +526,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
{item.type === "directory" ? ( + ) : item.type === "link" ? ( + ) : ( )} @@ -496,6 +568,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( !isOpen && (item.type === "directory" ? handlePathChange(item.path) + : item.type === "link" + ? handleSymlinkClick(item) : onOpenFile({ name: item.name, path: item.path, @@ -506,6 +580,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar( > {item.type === "directory" ? ( + ) : item.type === "link" ? ( + ) : ( )} @@ -514,7 +590,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
- {item.type === "file" && ( + {(item.type === "file") && (