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) => {
|
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];
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user