mirror of
https://github.com/crivion/laranode.git
synced 2026-05-07 22:20:44 +08:00
371 lines
16 KiB
JavaScript
371 lines
16 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { ImSpinner9 } from "react-icons/im";
|
|
import { VscFileSubmodule } from "react-icons/vsc";
|
|
import { FaFolderClosed } from "react-icons/fa6";
|
|
import { RiFolderReceivedLine } from "react-icons/ri";
|
|
import { FileIcon, defaultStyles } from 'react-file-icon';
|
|
import { ToastContainer, toast } from 'react-toastify';
|
|
import Checkbox from '@/Components/Checkbox';
|
|
import CreateFile from './Components/CreateFile';
|
|
import EditFile from './Components/EditFile';
|
|
import DeleteFiles from './Components/DeleteFiles';
|
|
import RenameFile from './Components/RenameFile';
|
|
import UploadFile from './Components/UploadFile';
|
|
|
|
import { LuFolderPlus } from "react-icons/lu";
|
|
import { LuFilePlus2 } from "react-icons/lu";
|
|
import { LuDelete } from "react-icons/lu";
|
|
import { MdOutlineDriveFileRenameOutline } from "react-icons/md";
|
|
import { IoMdCut } from "react-icons/io";
|
|
import { MdCopyAll } from "react-icons/md";
|
|
import { BiPaste } from "react-icons/bi";
|
|
import { TbFileTypeZip } from "react-icons/tb";
|
|
import { VscFileZip } from "react-icons/vsc";
|
|
import { FaUpload } from "react-icons/fa";
|
|
import { PiSelectionAllBold } from "react-icons/pi";
|
|
|
|
|
|
const Filemanager = () => {
|
|
|
|
const [files, setFiles] = useState([]);
|
|
const [path, setPath] = useState("/");
|
|
const [goBack, setGoBack] = useState(false);
|
|
const [spinner, showSpinner] = useState(true);
|
|
const [selectedPaths, setSelectedPaths] = useState([]);
|
|
const [createFileType, setCreateFileType] = useState(false);
|
|
const [editFile, setEditFile] = useState(false);
|
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
|
const [renameFile, setRenameFile] = useState(false);
|
|
const [showUploadFile, setShowUploadFile] = useState(false);
|
|
const [copyFiles, setCopyFiles] = useState(false);
|
|
const [cutFiles, setCutFiles] = useState(false);
|
|
|
|
|
|
useEffect(() => {
|
|
cdIntoPath(path);
|
|
}, [editFile]);
|
|
|
|
const cdIntoPath = async (path) => {
|
|
setPath(path);
|
|
showSpinner(true);
|
|
|
|
try {
|
|
const response = await fetch(`/filemanager/get-directory-contents?path=${path}`);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
const errorMessage = errorData.error || response.statusText;
|
|
toast(errorMessage, { type: 'error' });
|
|
showSpinner(false);
|
|
return;
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
// Decode the chunk and append to the buffer
|
|
buffer += decoder.decode(value, { stream: true });
|
|
}
|
|
|
|
const json = JSON.parse(buffer.trim());
|
|
setFiles(json.files);
|
|
setGoBack(json.goBack);
|
|
|
|
} catch (error) {
|
|
// Handle network errors or other exceptions
|
|
toast(error.message, { type: 'error' });
|
|
} finally {
|
|
showSpinner(false);
|
|
}
|
|
};
|
|
|
|
|
|
const handleFileClick = (file) => {
|
|
setSelectedPaths((prevSelected) =>
|
|
prevSelected.includes(file.path)
|
|
? prevSelected.filter((path) => path !== file.path)
|
|
: [...prevSelected, file.path]
|
|
);
|
|
};
|
|
|
|
const handleDoubleClick = (file) => {
|
|
|
|
if (cutFiles && selectedPaths.includes(file.path)) {
|
|
toast('Cannot cut files into a path that already contains them', { type: 'error' });
|
|
return;
|
|
}
|
|
|
|
if (file.type == "dir") {
|
|
cdIntoPath(file.path);
|
|
}
|
|
|
|
// do not edit images/videos based on extensions
|
|
const extension = file.path.split('.').pop();
|
|
const imagesAndVideos = ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mp3', 'webm', 'wav', 'ogg', 'flac', 'mkv', 'mov', 'avi', 'wmv', 'm4v'];
|
|
|
|
if (file.type == "file" && !imagesAndVideos.includes(extension)) {
|
|
setEditFile(file.path);
|
|
}
|
|
}
|
|
|
|
const selectAll = () => {
|
|
// toggle between all selected paths
|
|
if (selectedPaths.length === files.length) {
|
|
setSelectedPaths([]);
|
|
} else {
|
|
setSelectedPaths(files.map((file) => file.path));
|
|
}
|
|
};
|
|
|
|
const pasteFiles = async (pasteFromAction) => {
|
|
|
|
window.axios.patch('/filemanager/paste-files', { filesToPaste: selectedPaths, intoPath: path, pasteFromAction }).then((response) => {
|
|
setSelectedPaths([]);
|
|
setCutFiles(false);
|
|
cdIntoPath(path);
|
|
|
|
toast(response.data.message, { type: 'success' })
|
|
}).catch((error) => {
|
|
if (error?.response?.data?.error) {
|
|
toast(error.response.data.error, { type: 'error' });
|
|
} else {
|
|
toast(error.message, { type: 'error' });
|
|
}
|
|
console.log(error);
|
|
});
|
|
}
|
|
|
|
const formatBytes = (bytes, decimals = 2) => {
|
|
if (bytes === 0) return "0 B";
|
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
const factor = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${parseFloat((bytes / Math.pow(1024, factor)).toFixed(decimals))} ${sizes[factor]}`;
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
setShowConfirmDelete(true);
|
|
};
|
|
|
|
|
|
|
|
if (spinner) {
|
|
return (
|
|
<AuthenticatedLayout
|
|
header={
|
|
<div className="flex flex-col xl:justify-between xl:flex-row">
|
|
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight flex items-center">
|
|
<VscFileSubmodule className='mr-2' />
|
|
Filemanager
|
|
</h2>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="max-w-7xl bg-white dark:bg-gray-950 py-6 sm:px-6 lg:px-8">
|
|
<div className="mt-8 flex items-center space-x-2">
|
|
<div>
|
|
<ImSpinner9 className="animate-spin w-5 h-5 mr-2" />
|
|
</div>
|
|
<div className="text-gray-600 dark:text-gray-400 text-sm">Loading files list...</div>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
)
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
header={
|
|
<div className="flex flex-col xl:justify-between xl:flex-row">
|
|
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight flex items-center">
|
|
<VscFileSubmodule className='mr-2' />
|
|
Filemanager
|
|
</h2>
|
|
</div>
|
|
}
|
|
>
|
|
|
|
|
|
<div className="max-w-7xl">
|
|
<div className="mt-8 px-4">
|
|
|
|
<div className="mb-5 flex items-center space-x-2 text-gray-600 dark:text-gray-300">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:space-x-2 space-y-2 lg:space-y-0 font-semibold">
|
|
<button onClick={() => setShowUploadFile(true)} className="flex items-center hover:text-indigo-600">
|
|
<FaUpload className="mr-1" />
|
|
Upload
|
|
</button>
|
|
|
|
<button onClick={() => setCreateFileType('directory')} className="flex items-center hover:text-indigo-600">
|
|
<LuFolderPlus className="mr-1" />
|
|
Directory
|
|
</button>
|
|
<button onClick={() => setCreateFileType('file')} className="flex items-center hover:text-indigo-600">
|
|
<LuFilePlus2 className="mr-1" />
|
|
File
|
|
</button>
|
|
|
|
<button onClick={() => selectAll()} className="flex items-center hover:text-indigo-600">
|
|
<PiSelectionAllBold className="mr-1" />
|
|
{selectedPaths.length === files.length ? 'Deselect All' : 'Select All'}
|
|
</button>
|
|
|
|
|
|
<button onClick={() => setRenameFile(true)} className="flex items-center hover:text-indigo-600 disabled:opacity-25" disabled={selectedPaths.length != 1}>
|
|
<MdOutlineDriveFileRenameOutline className="mr-1" />
|
|
Rename
|
|
</button>
|
|
|
|
{!cutFiles ? (
|
|
<button onClick={() => setCutFiles(true)} className="flex items-center hover:text-indigo-600 disabled:opacity-25" disabled={selectedPaths.length == 0}>
|
|
<IoMdCut className="mr-1" />
|
|
Cut
|
|
</button>) : (
|
|
<button onClick={() => setCutFiles(false)} className="flex items-center hover:text-indigo-600 disabled:opacity-25">
|
|
<IoMdCut className="mr-1" />
|
|
Cancel
|
|
</button>
|
|
)
|
|
}
|
|
|
|
{cutFiles && (
|
|
<button onClick={() => pasteFiles('cut')} className="flex items-center hover:text-indigo-600 disabled:opacity-25">
|
|
<BiPaste className="mr-1" />
|
|
Paste
|
|
</button>
|
|
)}
|
|
|
|
<button onClick={() => setCopyFiles(true)} className="flex items-center hover:text-indigo-600 disabled:opacity-25" disabled={selectedPaths.length == 0 || cutFiles}>
|
|
<MdCopyAll className="mr-1" />
|
|
Copy -tbd
|
|
</button>
|
|
|
|
|
|
{copyFiles && (
|
|
<button onClick={() => pasteFiles('paste')} className="flex items-center hover:text-indigo-600 disabled:opacity-25">
|
|
<BiPaste className="mr-1" />
|
|
Paste
|
|
</button>
|
|
)}
|
|
|
|
<button onClick={() => confirmDelete()} className="flex items-center hover:text-red-600 disabled:opacity-25" disabled={selectedPaths.length == 0}>
|
|
<LuDelete className="mr-1" />
|
|
Delete
|
|
</button>
|
|
|
|
|
|
<button onClick={() => alert('TBD')} className="flex items-center hover:text-indigo-600 disabled:opacity-25" disabled={selectedPaths.length == 0}>
|
|
<TbFileTypeZip className="mr-1" />
|
|
Zip
|
|
</button>
|
|
|
|
{/* here make it if its only one file selected and it ends in .zip*/}
|
|
<button onClick={() => alert('TBD')} className="flex items-center hover:text-indigo-600 disabled:opacity-25" disabled={selectedPaths.length != 1 || !selectedPaths.some(file => file.endsWith('.zip'))}>
|
|
<VscFileZip className="mr-1" />
|
|
Extract
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{goBack && goBack != "" && (<div className='flex items-center space-x-2 text-xs'>
|
|
<div className="bg-white dark:bg-gray-850 py-3 px-6 hover:bg-gray-200 dark:hover:bg-gray-800">
|
|
<button className="dark:text-gray-300 text-gray-900 flex items-center space-x-2" onDoubleClick={() => cdIntoPath(goBack)}>
|
|
<RiFolderReceivedLine className="text-gray-500 dark:text-gray-300 mr-1" />
|
|
Back
|
|
</button>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-850 py-3 px-6 dark:text-gray-300 text-gray-900 flex items-center space-x-2">
|
|
Path: {path}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
|
|
{files.sort((a, b) => {
|
|
if (a.type === 'dir' && b.type !== 'dir') return -1;
|
|
if (a.type !== 'dir' && b.type === 'dir') return 1;
|
|
return 0;
|
|
}).map((file, index) => (
|
|
<div
|
|
key={`file-${index}`}
|
|
className={`flex items-center font-bold text-gray-900 dark:text-gray-300 py-3 px-6 border-b dark:border-b-gray-800 space-x-2 ${selectedPaths.includes(file.path)
|
|
? 'bg-gray-200 text-sky-700 hover:text-sky-700 hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-sky-600 dark:bg-gray-800 dark:text-sky-600'
|
|
: 'bg-white hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800'
|
|
}`}
|
|
onDoubleClick={() => handleDoubleClick(file)}
|
|
>
|
|
<div>
|
|
<Checkbox checked={selectedPaths.includes(file.path)} onChange={(e) => handleFileClick(file)} className="-mt-1 mr-1" />
|
|
</div>
|
|
{file.type === "dir" ? (
|
|
<div>
|
|
<FaFolderClosed className={`text-gray-500 ${selectedPaths.includes(file.path) ? 'text-sky-600' : ''} cursor-pointer`} />
|
|
</div>
|
|
) : (
|
|
<div className="w-4 h-4">
|
|
<FileIcon extension={file.path.split('.').pop()} {...defaultStyles[file.path.split('.').pop()]} className="cursor-pointer" />
|
|
</div>
|
|
)}
|
|
|
|
<div className={`text-sm cursor-pointer flex-grow flex items-center ${selectedPaths.includes(file.path) && cutFiles && 'text-gray-400'}`}>
|
|
{file.path.split('/').pop()}
|
|
{selectedPaths.includes(file.path) && cutFiles && <IoMdCut className="ml-1" />}
|
|
</div>
|
|
|
|
<div className="text-xs text-gray-400 dark:text-gray-600">
|
|
{typeof file.file_size == "undefined" ? "--" : formatBytes(file.file_size)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<CreateFile
|
|
path={path}
|
|
fileType={createFileType}
|
|
setCreateFileType={setCreateFileType}
|
|
refreshFiles={cdIntoPath}
|
|
/>
|
|
|
|
<EditFile
|
|
editFile={editFile}
|
|
setEditFile={setEditFile}
|
|
/>
|
|
|
|
<DeleteFiles
|
|
files={selectedPaths}
|
|
setSelectedPaths={setSelectedPaths}
|
|
showConfirmDelete={showConfirmDelete}
|
|
setShowConfirmDelete={setShowConfirmDelete}
|
|
refreshFiles={cdIntoPath}
|
|
path={path}
|
|
/>
|
|
|
|
<RenameFile
|
|
selectedFile={selectedPaths.length == 1 ? selectedPaths[0] : null}
|
|
setSelectedPaths={setSelectedPaths}
|
|
renameFile={renameFile}
|
|
setRenameFile={setRenameFile}
|
|
refreshFiles={cdIntoPath}
|
|
path={path}
|
|
/>
|
|
|
|
<UploadFile
|
|
showUploadFile={showUploadFile}
|
|
setShowUploadFile={setShowUploadFile}
|
|
refreshFiles={cdIntoPath}
|
|
path={path}
|
|
/>
|
|
|
|
</AuthenticatedLayout >
|
|
);
|
|
}
|
|
|
|
export default Filemanager
|