import React, { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; import { Card } from "@/components/ui/card.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { Upload, Download, FilePlus, FolderPlus, Trash2, Edit3, X, AlertCircle, FileText, Folder, } from "lucide-react"; import { cn } from "@/lib/utils.ts"; import { useTranslation } from "react-i18next"; import type { FileManagerOperationsProps } from "../../../types/index.js"; export function FileManagerOperations({ currentPath, sshSessionId, onOperationComplete, onError, onSuccess, }: FileManagerOperationsProps) { const { t } = useTranslation(); const [showUpload, setShowUpload] = useState(false); const [showDownload, setShowDownload] = useState(false); const [showCreateFile, setShowCreateFile] = useState(false); const [showCreateFolder, setShowCreateFolder] = useState(false); const [showDelete, setShowDelete] = useState(false); const [showRename, setShowRename] = useState(false); const [uploadFile, setUploadFile] = useState(null); const [downloadPath, setDownloadPath] = useState(""); const [newFileName, setNewFileName] = useState(""); const [newFolderName, setNewFolderName] = useState(""); const [deletePath, setDeletePath] = useState(""); const [deleteIsDirectory, setDeleteIsDirectory] = useState(false); const [renamePath, setRenamePath] = useState(""); const [renameIsDirectory, setRenameIsDirectory] = useState(false); const [newName, setNewName] = useState(""); const [isLoading, setIsLoading] = useState(false); const [showTextLabels, setShowTextLabels] = useState(true); const fileInputRef = useRef(null); const containerRef = useRef(null); useEffect(() => { const checkContainerWidth = () => { if (containerRef.current) { const width = containerRef.current.offsetWidth; setShowTextLabels(width > 240); } }; checkContainerWidth(); const resizeObserver = new ResizeObserver(checkContainerWidth); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => { resizeObserver.disconnect(); }; }, []); const handleFileUpload = async () => { if (!uploadFile || !sshSessionId) return; setIsLoading(true); const { toast } = await import("sonner"); const loadingToast = toast.loading( t("fileManager.uploadingFile", { name: uploadFile.name }), ); try { // Read file content - support text and binary files const content = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(reader.error); // Check file type to determine reading method const isTextFile = uploadFile.type.startsWith("text/") || uploadFile.type === "application/json" || uploadFile.type === "application/javascript" || uploadFile.type === "application/xml" || uploadFile.name.match( /\.(txt|json|js|ts|jsx|tsx|css|html|htm|xml|yaml|yml|md|py|java|c|cpp|h|sh|bat|ps1)$/i, ); if (isTextFile) { reader.onload = () => { if (reader.result) { resolve(reader.result as string); } else { reject(new Error("Failed to read text file content")); } }; reader.readAsText(uploadFile); } else { reader.onload = () => { if (reader.result instanceof ArrayBuffer) { const bytes = new Uint8Array(reader.result); let binary = ""; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } const base64 = btoa(binary); resolve(base64); } else { reject(new Error("Failed to read binary file")); } }; reader.readAsArrayBuffer(uploadFile); } }); const { uploadSSHFile } = await import("@/ui/main-axios.ts"); const response = await uploadSSHFile( sshSessionId, currentPath, uploadFile.name, content, ); toast.dismiss(loadingToast); if (response?.toast) { toast[response.toast.type](response.toast.message); } else { onSuccess( t("fileManager.fileUploadedSuccessfully", { name: uploadFile.name }), ); } setShowUpload(false); setUploadFile(null); onOperationComplete(); } catch (error: any) { toast.dismiss(loadingToast); onError( error?.response?.data?.error || t("fileManager.failedToUploadFile"), ); } finally { setIsLoading(false); } }; const handleCreateFile = async () => { if (!newFileName.trim() || !sshSessionId) return; setIsLoading(true); const { toast } = await import("sonner"); const loadingToast = toast.loading( t("fileManager.creatingFile", { name: newFileName.trim() }), ); try { const { createSSHFile } = await import("@/ui/main-axios.ts"); const response = await createSSHFile( sshSessionId, currentPath, newFileName.trim(), ); toast.dismiss(loadingToast); if (response?.toast) { toast[response.toast.type](response.toast.message); } else { onSuccess( t("fileManager.fileCreatedSuccessfully", { name: newFileName.trim(), }), ); } setShowCreateFile(false); setNewFileName(""); onOperationComplete(); } catch (error: any) { toast.dismiss(loadingToast); onError( error?.response?.data?.error || t("fileManager.failedToCreateFile"), ); } finally { setIsLoading(false); } }; const handleDownload = async () => { if (!downloadPath.trim() || !sshSessionId) return; setIsLoading(true); const { toast } = await import("sonner"); const fileName = downloadPath.split("/").pop() || "download"; const loadingToast = toast.loading( t("fileManager.downloadingFile", { name: fileName }), ); try { const { downloadSSHFile } = await import("@/ui/main-axios.ts"); const response = await downloadSSHFile(sshSessionId, downloadPath.trim()); toast.dismiss(loadingToast); if (response?.content) { // Convert base64 to blob and trigger download const byteCharacters = atob(response.content); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: response.mimeType || "application/octet-stream", }); // Create download link const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = response.fileName || fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); onSuccess( t("fileManager.fileDownloadedSuccessfully", { name: response.fileName || fileName, }), ); } else { onError(t("fileManager.noFileContent")); } setShowDownload(false); setDownloadPath(""); } catch (error: any) { toast.dismiss(loadingToast); onError( error?.response?.data?.error || t("fileManager.failedToDownloadFile"), ); } finally { setIsLoading(false); } }; const handleCreateFolder = async () => { if (!newFolderName.trim() || !sshSessionId) return; setIsLoading(true); const { toast } = await import("sonner"); const loadingToast = toast.loading( t("fileManager.creatingFolder", { name: newFolderName.trim() }), ); try { const { createSSHFolder } = await import("@/ui/main-axios.ts"); const response = await createSSHFolder( sshSessionId, currentPath, newFolderName.trim(), ); toast.dismiss(loadingToast); if (response?.toast) { toast[response.toast.type](response.toast.message); } else { onSuccess( t("fileManager.folderCreatedSuccessfully", { name: newFolderName.trim(), }), ); } setShowCreateFolder(false); setNewFolderName(""); onOperationComplete(); } catch (error: any) { toast.dismiss(loadingToast); onError( error?.response?.data?.error || t("fileManager.failedToCreateFolder"), ); } finally { setIsLoading(false); } }; const handleDelete = async () => { if (!deletePath || !sshSessionId) return; setIsLoading(true); const { toast } = await import("sonner"); const loadingToast = toast.loading( t("fileManager.deletingItem", { type: deleteIsDirectory ? t("fileManager.folder") : t("fileManager.file"), name: deletePath.split("/").pop(), }), ); try { const { deleteSSHItem } = await import("@/ui/main-axios.ts"); const response = await deleteSSHItem( sshSessionId, deletePath, deleteIsDirectory, ); toast.dismiss(loadingToast); if (response?.toast) { toast[response.toast.type](response.toast.message); } else { onSuccess( t("fileManager.itemDeletedSuccessfully", { type: deleteIsDirectory ? t("fileManager.folder") : t("fileManager.file"), }), ); } setShowDelete(false); setDeletePath(""); setDeleteIsDirectory(false); onOperationComplete(); } catch (error: any) { toast.dismiss(loadingToast); onError( error?.response?.data?.error || t("fileManager.failedToDeleteItem"), ); } finally { setIsLoading(false); } }; const handleRename = async () => { if (!renamePath || !newName.trim() || !sshSessionId) return; setIsLoading(true); const { toast } = await import("sonner"); const loadingToast = toast.loading( t("fileManager.renamingItem", { type: renameIsDirectory ? t("fileManager.folder") : t("fileManager.file"), oldName: renamePath.split("/").pop(), newName: newName.trim(), }), ); try { const { renameSSHItem } = await import("@/ui/main-axios.ts"); const response = await renameSSHItem( sshSessionId, renamePath, newName.trim(), ); toast.dismiss(loadingToast); if (response?.toast) { toast[response.toast.type](response.toast.message); } else { onSuccess( t("fileManager.itemRenamedSuccessfully", { type: renameIsDirectory ? t("fileManager.folder") : t("fileManager.file"), }), ); } setShowRename(false); setRenamePath(""); setRenameIsDirectory(false); setNewName(""); onOperationComplete(); } catch (error: any) { toast.dismiss(loadingToast); onError( error?.response?.data?.error || t("fileManager.failedToRenameItem"), ); } finally { setIsLoading(false); } }; const openFileDialog = () => { fileInputRef.current?.click(); }; const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setUploadFile(file); } }; const resetStates = () => { setShowUpload(false); setShowCreateFile(false); setShowCreateFolder(false); setShowDelete(false); setShowRename(false); setUploadFile(null); setNewFileName(""); setNewFolderName(""); setDeletePath(""); setDeleteIsDirectory(false); setRenamePath(""); setRenameIsDirectory(false); setNewName(""); }; if (!sshSessionId) { return (

{t("fileManager.connectToSsh")}

); } return (
{t("fileManager.currentPath")}: {currentPath}
{showUpload && (

{t("fileManager.uploadFileTitle")}

{t("fileManager.maxFileSize")}

{uploadFile ? (

{uploadFile.name}

{(uploadFile.size / 1024).toFixed(2)} KB

) : (

{t("fileManager.clickToSelectFile")}

)}
)} {showDownload && (

{t("fileManager.downloadFile")}

setDownloadPath(e.target.value)} placeholder={t("placeholders.fullPath")} className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm" onKeyDown={(e) => e.key === "Enter" && handleDownload()} />
)} {showCreateFile && (

{t("fileManager.createNewFile")}

setNewFileName(e.target.value)} placeholder={t("placeholders.fileName")} className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm" onKeyDown={(e) => e.key === "Enter" && handleCreateFile()} />
)} {showCreateFolder && (

{t("fileManager.createNewFolder")}

setNewFolderName(e.target.value)} placeholder={t("placeholders.folderName")} className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm" onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()} />
)} {showDelete && (

{t("fileManager.deleteItem")}

{t("fileManager.warningCannotUndo")}
setDeletePath(e.target.value)} placeholder={t("placeholders.fullPath")} className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm" />
setDeleteIsDirectory(e.target.checked)} className="rounded border-dark-border-hover bg-dark-bg-button mt-0.5 flex-shrink-0" />
)} {showRename && (

{t("fileManager.renameItem")}

setRenamePath(e.target.value)} placeholder={t("placeholders.currentPath")} className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm" />
setNewName(e.target.value)} placeholder={t("placeholders.newName")} className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm" onKeyDown={(e) => e.key === "Enter" && handleRename()} />
setRenameIsDirectory(e.target.checked)} className="rounded border-dark-border-hover bg-dark-bg-button mt-0.5 flex-shrink-0" />
)}
); }