feat: Add file permissions dialog for SSH file manager
Add a comprehensive file permissions modification feature accessible via right-click context menu: - Add PermissionsDialog component with read/write/execute checkboxes for owner/group/others - Display current and new permissions side-by-side with real-time octal calculation - Support both numeric (755) and symbolic (rwxr-xr-x) permission formats - Implement backend API endpoint POST /ssh/file_manager/ssh/changePermissions - Add frontend API function changeSSHPermissions with complete logging - Integrate dialog with FileManager via onProperties callback - Add i18n translations for English and Chinese - Include path escaping and comprehensive error handling
This commit is contained in:
@@ -2490,6 +2490,103 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => {
|
||||||
|
const { sessionId, path, permissions } = req.body;
|
||||||
|
const sshConn = sshSessions[sessionId];
|
||||||
|
|
||||||
|
if (!sshConn || !sshConn.isConnected) {
|
||||||
|
fileLogger.error(
|
||||||
|
"SSH connection not found or not connected for changePermissions",
|
||||||
|
{
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
hasConnection: !!sshConn,
|
||||||
|
isConnected: sshConn?.isConnected,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return res.status(400).json({ error: "SSH connection not available" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return res.status(400).json({ error: "File path is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permissions || !/^\d{3,4}$/.test(permissions)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Valid permissions required (e.g., 755, 644)"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const octalPerms = permissions.slice(-3);
|
||||||
|
const escapedPath = path.replace(/'/g, "'\"'\"'");
|
||||||
|
const command = `chmod ${octalPerms} '${escapedPath}'`;
|
||||||
|
|
||||||
|
fileLogger.info("Changing file permissions", {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions: octalPerms,
|
||||||
|
});
|
||||||
|
|
||||||
|
sshConn.client.exec(command, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
fileLogger.error("SSH changePermissions exec error:", err, {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions: octalPerms,
|
||||||
|
});
|
||||||
|
return res.status(500).json({ error: "Failed to change permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorOutput = "";
|
||||||
|
|
||||||
|
stream.stderr.on("data", (data) => {
|
||||||
|
errorOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
fileLogger.error("chmod command failed", {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions: octalPerms,
|
||||||
|
exitCode: code,
|
||||||
|
error: errorOutput,
|
||||||
|
});
|
||||||
|
return res.status(500).json({
|
||||||
|
error: errorOutput || "Failed to change permissions"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLogger.success("File permissions changed successfully", {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions: octalPerms,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Permissions changed successfully"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on("error", (streamErr) => {
|
||||||
|
fileLogger.error("SSH changePermissions stream error:", streamErr, {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions: octalPerms,
|
||||||
|
});
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: "Stream error while changing permissions" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
Object.keys(sshSessions).forEach(cleanupSession);
|
Object.keys(sshSessions).forEach(cleanupSession);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -1169,7 +1169,19 @@
|
|||||||
"sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})",
|
"sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})",
|
||||||
"loadFileFailed": "Failed to load file: {{error}}",
|
"loadFileFailed": "Failed to load file: {{error}}",
|
||||||
"connectedSuccessfully": "Connected successfully",
|
"connectedSuccessfully": "Connected successfully",
|
||||||
"totpVerificationFailed": "TOTP verification failed"
|
"totpVerificationFailed": "TOTP verification failed",
|
||||||
|
"changePermissions": "Change Permissions",
|
||||||
|
"changePermissionsDesc": "Modify file permissions for",
|
||||||
|
"currentPermissions": "Current Permissions",
|
||||||
|
"newPermissions": "New Permissions",
|
||||||
|
"owner": "Owner",
|
||||||
|
"group": "Group",
|
||||||
|
"others": "Others",
|
||||||
|
"read": "Read",
|
||||||
|
"write": "Write",
|
||||||
|
"execute": "Execute",
|
||||||
|
"permissionsChangedSuccessfully": "Permissions changed successfully",
|
||||||
|
"failedToChangePermissions": "Failed to change permissions"
|
||||||
},
|
},
|
||||||
"tunnels": {
|
"tunnels": {
|
||||||
"title": "SSH Tunnels",
|
"title": "SSH Tunnels",
|
||||||
|
|||||||
@@ -1151,7 +1151,19 @@
|
|||||||
"sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接",
|
"sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接",
|
||||||
"loadFileFailed": "加载文件失败:{{error}}",
|
"loadFileFailed": "加载文件失败:{{error}}",
|
||||||
"connectedSuccessfully": "连接成功",
|
"connectedSuccessfully": "连接成功",
|
||||||
"totpVerificationFailed": "TOTP 验证失败"
|
"totpVerificationFailed": "TOTP 验证失败",
|
||||||
|
"changePermissions": "修改权限",
|
||||||
|
"changePermissionsDesc": "修改文件权限",
|
||||||
|
"currentPermissions": "当前权限",
|
||||||
|
"newPermissions": "新权限",
|
||||||
|
"owner": "所有者",
|
||||||
|
"group": "组",
|
||||||
|
"others": "其他",
|
||||||
|
"read": "读取",
|
||||||
|
"write": "写入",
|
||||||
|
"execute": "执行",
|
||||||
|
"permissionsChangedSuccessfully": "权限修改成功",
|
||||||
|
"failedToChangePermissions": "权限修改失败"
|
||||||
},
|
},
|
||||||
"tunnels": {
|
"tunnels": {
|
||||||
"title": "SSH 隧道",
|
"title": "SSH 隧道",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { toast } from "sonner";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx";
|
import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx";
|
||||||
import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx";
|
import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx";
|
||||||
|
import { PermissionsDialog } from "./components/PermissionsDialog";
|
||||||
import {
|
import {
|
||||||
Upload,
|
Upload,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
@@ -49,6 +50,7 @@ import {
|
|||||||
addFolderShortcut,
|
addFolderShortcut,
|
||||||
getPinnedFiles,
|
getPinnedFiles,
|
||||||
logActivity,
|
logActivity,
|
||||||
|
changeSSHPermissions,
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import type { SidebarItem } from "./FileManagerSidebar";
|
import type { SidebarItem } from "./FileManagerSidebar";
|
||||||
|
|
||||||
@@ -146,6 +148,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
|
|
||||||
const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null);
|
const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null);
|
||||||
const [editingFile, setEditingFile] = useState<FileItem | null>(null);
|
const [editingFile, setEditingFile] = useState<FileItem | null>(null);
|
||||||
|
const [permissionsDialogFile, setPermissionsDialogFile] = useState<FileItem | null>(null);
|
||||||
|
|
||||||
const { selectedFiles, clearSelection, setSelection } = useFileSelection();
|
const { selectedFiles, clearSelection, setSelection } = useFileSelection();
|
||||||
|
|
||||||
@@ -1180,6 +1183,34 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
setEditingFile(file);
|
setEditingFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleOpenPermissionsDialog(file: FileItem) {
|
||||||
|
setPermissionsDialogFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSavePermissions(file: FileItem, permissions: string) {
|
||||||
|
if (!sshSessionId) {
|
||||||
|
toast.error(t("fileManager.noSSHConnection"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await changeSSHPermissions(
|
||||||
|
sshSessionId,
|
||||||
|
file.path,
|
||||||
|
permissions,
|
||||||
|
currentHost?.id,
|
||||||
|
currentHost?.userId?.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.success(t("fileManager.permissionsChangedSuccessfully"));
|
||||||
|
await handleRefreshDirectory();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Failed to change permissions:", error);
|
||||||
|
toast.error(t("fileManager.failedToChangePermissions"));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureSSHConnection() {
|
async function ensureSSHConnection() {
|
||||||
if (!sshSessionId || !currentHost || isReconnecting) return;
|
if (!sshSessionId || !currentHost || isReconnecting) return;
|
||||||
|
|
||||||
@@ -1968,6 +1999,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
onAddShortcut={handleAddShortcut}
|
onAddShortcut={handleAddShortcut}
|
||||||
isPinned={isPinnedFile}
|
isPinned={isPinnedFile}
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
|
onProperties={handleOpenPermissionsDialog}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1993,6 +2025,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<PermissionsDialog
|
||||||
|
file={permissionsDialogFile}
|
||||||
|
open={permissionsDialogFile !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setPermissionsDialogFile(null);
|
||||||
|
}}
|
||||||
|
onSave={handleSavePermissions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Shield } from "lucide-react";
|
||||||
|
|
||||||
|
interface FileItem {
|
||||||
|
name: string;
|
||||||
|
type: "file" | "directory" | "link";
|
||||||
|
path: string;
|
||||||
|
permissions?: string;
|
||||||
|
owner?: string;
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionsDialogProps {
|
||||||
|
file: FileItem | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (file: FileItem, permissions: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse permissions like "rwxr-xr-x" or "755" to individual bits
|
||||||
|
const parsePermissions = (perms: string): { owner: number; group: number; other: number } => {
|
||||||
|
if (!perms) {
|
||||||
|
return { owner: 0, group: 0, other: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If numeric format like "755"
|
||||||
|
if (/^\d{3,4}$/.test(perms)) {
|
||||||
|
const numStr = perms.slice(-3);
|
||||||
|
return {
|
||||||
|
owner: parseInt(numStr[0] || "0", 10),
|
||||||
|
group: parseInt(numStr[1] || "0", 10),
|
||||||
|
other: parseInt(numStr[2] || "0", 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If symbolic format like "rwxr-xr-x" or "-rwxr-xr-x"
|
||||||
|
const cleanPerms = perms.replace(/^-/, "").substring(0, 9);
|
||||||
|
|
||||||
|
const calcBits = (str: string): number => {
|
||||||
|
let value = 0;
|
||||||
|
if (str[0] === "r") value += 4;
|
||||||
|
if (str[1] === "w") value += 2;
|
||||||
|
if (str[2] === "x") value += 1;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
owner: calcBits(cleanPerms.substring(0, 3)),
|
||||||
|
group: calcBits(cleanPerms.substring(3, 6)),
|
||||||
|
other: calcBits(cleanPerms.substring(6, 9)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert individual bits to numeric format
|
||||||
|
const toNumeric = (owner: number, group: number, other: number): string => {
|
||||||
|
return `${owner}${group}${other}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PermissionsDialog({
|
||||||
|
file,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
}: PermissionsDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const initialPerms = parsePermissions(file?.permissions || "644");
|
||||||
|
const [ownerRead, setOwnerRead] = useState((initialPerms.owner & 4) !== 0);
|
||||||
|
const [ownerWrite, setOwnerWrite] = useState((initialPerms.owner & 2) !== 0);
|
||||||
|
const [ownerExecute, setOwnerExecute] = useState((initialPerms.owner & 1) !== 0);
|
||||||
|
|
||||||
|
const [groupRead, setGroupRead] = useState((initialPerms.group & 4) !== 0);
|
||||||
|
const [groupWrite, setGroupWrite] = useState((initialPerms.group & 2) !== 0);
|
||||||
|
const [groupExecute, setGroupExecute] = useState((initialPerms.group & 1) !== 0);
|
||||||
|
|
||||||
|
const [otherRead, setOtherRead] = useState((initialPerms.other & 4) !== 0);
|
||||||
|
const [otherWrite, setOtherWrite] = useState((initialPerms.other & 2) !== 0);
|
||||||
|
const [otherExecute, setOtherExecute] = useState((initialPerms.other & 1) !== 0);
|
||||||
|
|
||||||
|
// Reset when file changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (file) {
|
||||||
|
const perms = parsePermissions(file.permissions || "644");
|
||||||
|
setOwnerRead((perms.owner & 4) !== 0);
|
||||||
|
setOwnerWrite((perms.owner & 2) !== 0);
|
||||||
|
setOwnerExecute((perms.owner & 1) !== 0);
|
||||||
|
setGroupRead((perms.group & 4) !== 0);
|
||||||
|
setGroupWrite((perms.group & 2) !== 0);
|
||||||
|
setGroupExecute((perms.group & 1) !== 0);
|
||||||
|
setOtherRead((perms.other & 4) !== 0);
|
||||||
|
setOtherWrite((perms.other & 2) !== 0);
|
||||||
|
setOtherExecute((perms.other & 1) !== 0);
|
||||||
|
}
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
const calculateOctal = (): string => {
|
||||||
|
const owner = (ownerRead ? 4 : 0) + (ownerWrite ? 2 : 0) + (ownerExecute ? 1 : 0);
|
||||||
|
const group = (groupRead ? 4 : 0) + (groupWrite ? 2 : 0) + (groupExecute ? 1 : 0);
|
||||||
|
const other = (otherRead ? 4 : 0) + (otherWrite ? 2 : 0) + (otherExecute ? 1 : 0);
|
||||||
|
return toNumeric(owner, group, other);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const permissions = calculateOctal();
|
||||||
|
await onSave(file, permissions);
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update permissions:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!file) return null;
|
||||||
|
|
||||||
|
const octal = calculateOctal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-dark-bg border-2 border-dark-border">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="w-5 h-5" />
|
||||||
|
{t("fileManager.changePermissions")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
{t("fileManager.changePermissionsDesc")}: <span className="font-mono text-foreground">{file.name}</span>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Current info */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-400">{t("fileManager.currentPermissions")}</Label>
|
||||||
|
<p className="font-mono text-lg mt-1">{file.permissions || "644"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-400">{t("fileManager.newPermissions")}</Label>
|
||||||
|
<p className="font-mono text-lg mt-1">{octal}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Owner permissions */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-base font-semibold text-foreground">
|
||||||
|
{t("fileManager.owner")} {file.owner && `(${file.owner})`}
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-6 ml-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="owner-read"
|
||||||
|
checked={ownerRead}
|
||||||
|
onCheckedChange={(checked) => setOwnerRead(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="owner-read" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.read")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="owner-write"
|
||||||
|
checked={ownerWrite}
|
||||||
|
onCheckedChange={(checked) => setOwnerWrite(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="owner-write" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.write")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="owner-execute"
|
||||||
|
checked={ownerExecute}
|
||||||
|
onCheckedChange={(checked) => setOwnerExecute(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="owner-execute" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.execute")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Group permissions */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-base font-semibold text-foreground">
|
||||||
|
{t("fileManager.group")} {file.group && `(${file.group})`}
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-6 ml-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="group-read"
|
||||||
|
checked={groupRead}
|
||||||
|
onCheckedChange={(checked) => setGroupRead(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="group-read" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.read")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="group-write"
|
||||||
|
checked={groupWrite}
|
||||||
|
onCheckedChange={(checked) => setGroupWrite(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="group-write" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.write")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="group-execute"
|
||||||
|
checked={groupExecute}
|
||||||
|
onCheckedChange={(checked) => setGroupExecute(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="group-execute" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.execute")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Others permissions */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-base font-semibold text-foreground">
|
||||||
|
{t("fileManager.others")}
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-6 ml-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="other-read"
|
||||||
|
checked={otherRead}
|
||||||
|
onCheckedChange={(checked) => setOtherRead(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="other-read" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.read")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="other-write"
|
||||||
|
checked={otherWrite}
|
||||||
|
onCheckedChange={(checked) => setOtherWrite(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="other-write" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.write")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="other-execute"
|
||||||
|
checked={otherExecute}
|
||||||
|
onCheckedChange={(checked) => setOtherExecute(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="other-execute" className="text-sm cursor-pointer">
|
||||||
|
{t("fileManager.execute")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={loading}>
|
||||||
|
{loading ? t("common.saving") : t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1515,6 +1515,51 @@ export async function moveSSHItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function changeSSHPermissions(
|
||||||
|
sessionId: string,
|
||||||
|
path: string,
|
||||||
|
permissions: string,
|
||||||
|
hostId?: number,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
fileLogger.info("Changing SSH file permissions", {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions,
|
||||||
|
hostId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fileManagerApi.post("/ssh/changePermissions", {
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions,
|
||||||
|
hostId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
fileLogger.success("SSH file permissions changed successfully", {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
fileLogger.error("Failed to change SSH file permissions", error, {
|
||||||
|
operation: "change_permissions",
|
||||||
|
sessionId,
|
||||||
|
path,
|
||||||
|
permissions,
|
||||||
|
});
|
||||||
|
handleApiError(error, "change SSH permissions");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FILE MANAGER DATA
|
// FILE MANAGER DATA
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user