feat: Added function to handle symlink

This commit is contained in:
jedi04
2025-09-13 01:42:03 +05:30
parent d4dc310c93
commit d5471d68a3
3 changed files with 157 additions and 2 deletions

View File

@@ -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) => { app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const sessionId = req.query.sessionId as string; const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId]; const sshConn = sshSessions[sessionId];

View File

@@ -8,6 +8,7 @@ import React, {
import { import {
Folder, Folder,
File, File,
FileSymlink,
ArrowUp, ArrowUp,
Pin, Pin,
MoreVertical, MoreVertical,
@@ -29,6 +30,7 @@ import {
removeFileManagerPinned, removeFileManagerPinned,
getSSHStatus, getSSHStatus,
connectSSH, connectSSH,
identifySSHSymlink,
} from "@/ui/main-axios.ts"; } from "@/ui/main-axios.ts";
import type { SSHHost } from "../../../types/index.js"; import type { SSHHost } from "../../../types/index.js";
@@ -339,7 +341,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
try { try {
await renameSSHItem(sshSessionId, item.path, newName.trim()); await renameSSHItem(sshSessionId, item.path, newName.trim());
toast.success( 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); setRenamingItem(null);
if (onOperationComplete) { if (onOperationComplete) {
@@ -375,6 +377,74 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
onPathChange?.(newPath); 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 ( return (
<div className="flex flex-col h-full w-[256px] max-w-[256px]"> <div className="flex flex-col h-full w-[256px] max-w-[256px]">
<div className="flex flex-col flex-grow min-h-0"> <div className="flex flex-col flex-grow min-h-0">
@@ -456,6 +526,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
{item.type === "directory" ? ( {item.type === "directory" ? (
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0" /> <Folder className="w-4 h-4 text-blue-400 flex-shrink-0" />
) : item.type === "link" ? (
<FileSymlink className="w-4 h-4 text-blue-400 flex-shrink-0" />
) : ( ) : (
<File className="w-4 h-4 text-muted-foreground flex-shrink-0" /> <File className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)} )}
@@ -496,6 +568,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
!isOpen && !isOpen &&
(item.type === "directory" (item.type === "directory"
? handlePathChange(item.path) ? handlePathChange(item.path)
: item.type === "link"
? handleSymlinkClick(item)
: onOpenFile({ : onOpenFile({
name: item.name, name: item.name,
path: item.path, path: item.path,
@@ -506,6 +580,8 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
> >
{item.type === "directory" ? ( {item.type === "directory" ? (
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0" /> <Folder className="w-4 h-4 text-blue-400 flex-shrink-0" />
) : item.type === "link" ? (
<FileSymlink className="w-4 h-4 text-blue-400 flex-shrink-0" />
) : ( ) : (
<File className="w-4 h-4 text-muted-foreground flex-shrink-0" /> <File className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)} )}
@@ -514,7 +590,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
</span> </span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{item.type === "file" && ( {(item.type === "file") && (
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"

View File

@@ -969,6 +969,20 @@ export async function listSSHFiles(
} }
} }
export async function identifySSHSymlink(
sessionId: string,
path: string,
): Promise<{ path: string; target: string; type: "directory" | "file" }> {
try {
const response = await fileManagerApi.get("/ssh/identifySymlink", {
params: { sessionId, path },
});
return response.data;
} catch (error) {
handleApiError(error, "identify SSH symlink");
}
}
export async function readSSHFile( export async function readSSHFile(
sessionId: string, sessionId: string,
path: string, path: string,