feat: Added function to handle symlink
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col h-full w-[256px] max-w-[256px]">
|
||||
<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">
|
||||
{item.type === "directory" ? (
|
||||
<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" />
|
||||
)}
|
||||
@@ -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" ? (
|
||||
<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" />
|
||||
)}
|
||||
@@ -514,7 +590,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{item.type === "file" && (
|
||||
{(item.type === "file") && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@@ -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(
|
||||
sessionId: string,
|
||||
path: string,
|
||||
|
||||
Reference in New Issue
Block a user