import React from "react"; import { Folder, FolderElement, FolderElementType } from "./components/folder"; import { Filesystem } from "./filesystem"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "./components/ui/button"; import { Input } from "./components/ui/input"; import { DialogDescription } from "@radix-ui/react-dialog"; import Dropzone from "react-dropzone"; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { HouseFill } from "react-bootstrap-icons"; export interface FilesystemExplorerProps { filesystem: Filesystem; runFile: (file: string) => void; resetFilesys: () => void; path: string[]; } export interface FilesystemExplorerState { path: string[]; createFolder: boolean; resetFilesys: boolean; errorText: string; removeFile: string; renameFile: string; } function makeFullPath(path: string[]) { return "/root/filesys/" + path.join("/"); } function makeFullPathAndJoin(path: string[], element: string) { return makeFullPath([...path, element]); } function makeFullPathWithState( state: FilesystemExplorerState, element: string, ) { return makeFullPathAndJoin(state.path, element); } function relativePathToWindowsPath(fullPath: string) { if (fullPath.length == 0) { return fullPath; } const drive = fullPath.substring(0, 1); const rest = fullPath.substring(1); return `${drive}:${rest}`; } function makeRelativePathWithState( state: FilesystemExplorerState, element: string, ) { return [...state.path, element].join("/"); } function makeWindowsPathWithState( state: FilesystemExplorerState, element: string, ) { const fullPath = makeRelativePathWithState(state, element); return relativePathToWindowsPath(fullPath); } function getFolderElements(filesystem: Filesystem, path: string[]) { const fullPath = makeFullPath(path); const files = filesystem.readDir(fullPath); return files .filter((f) => { if (f == ".") { return false; } if (path.length == 0 && f == "..") { return false; } return true; }) .map((f) => { const element: FolderElement = { name: f, type: filesystem.isFolder(`${fullPath}/${f}`) ? FolderElementType.Folder : FolderElementType.File, }; return element; }); } interface FileWithData { file: File; data: ArrayBuffer; } function readFile(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (reader.readyState === FileReader.DONE) { resolve({ file, data: reader.result as ArrayBuffer, }); } }; reader.onerror = reject; reader.readAsArrayBuffer(file); }); } async function readFiles(files: FileList | File[]): Promise { const promises = []; for (let i = 0; i < files.length; i++) { promises.push(readFile(files[i])); } return Promise.all(promises); } function selectFiles(): Promise { return new Promise((resolve) => { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".exe"; fileInput.addEventListener("change", function (event) { const files = (event as any).target.files as FileList; resolve(files); }); fileInput.click(); }); } interface BreadcrumbElement { node: React.ReactNode; targetPath: string[]; } function generateBreadcrumbElements(path: string[]): BreadcrumbElement[] { const elements = path.map((p, index) => { const e: BreadcrumbElement = { node: p, targetPath: path.slice(0, index + 1), }; return e; }); elements.unshift({ node: , targetPath: [], }); return elements; } export class FilesystemExplorer extends React.Component< FilesystemExplorerProps, FilesystemExplorerState > { constructor(props: FilesystemExplorerProps) { super(props); this._onAddFiles = this._onAddFiles.bind(this); this._uploadFiles = this._uploadFiles.bind(this); this._onElementSelect = this._onElementSelect.bind(this); this.state = { path: this.props.path, createFolder: false, resetFilesys: false, errorText: "", removeFile: "", renameFile: "", }; } _showError(errorText: string) { this.setState({ errorText }); } _onElementSelect(element: FolderElement) { if (element.type != FolderElementType.Folder) { if (element.name.endsWith(".exe")) { const file = makeWindowsPathWithState(this.state, element.name); this.props.runFile(file); } return; } this.setState((s) => { const path = [...s.path]; if (element.name == "..") { path.pop(); } else { path.push(element.name); } return { path, }; }); } async _onFileRename(file: string, newFile: string) { const oldPath = makeFullPathWithState(this.state, file); const newPath = makeFullPathWithState(this.state, newFile); this.setState({ renameFile: "" }); await this.props.filesystem.rename(oldPath, newPath); this.forceUpdate(); } async _onAddFiles() { const files = await selectFiles(); await this._uploadFiles(files); } async _onFolderCreate(name: string) { this.setState({ createFolder: false }); name = name.toLowerCase(); if (name.length == 0) { return; } if (name.includes("/") || name.includes("\\")) { this._showError("Folder must not contain special characters"); return; } if (this.state.path.length == 0 && name.length > 1) { this._showError("Drives must be a single letter"); return; } const fullPath = makeFullPathWithState(this.state, name); await this.props.filesystem.createFolder(fullPath); this.forceUpdate(); } async _uploadFiles(files: FileList | File[]) { if (files.length == 0) { return; } if (this.state.path.length == 0) { this._showError("Files must be within a drive"); return; } const fileData = (await readFiles(files)).map((f) => { return { name: makeFullPathWithState(this.state, f.file.name.toLowerCase()), data: f.data, }; }); await this.props.filesystem.storeFiles(fileData); this.forceUpdate(); } _renderCreateFolderDialog() { return ( this.setState({ createFolder: open })} >
{ const folderName = (e.target as any).elements.name.value; this._onFolderCreate(folderName); e.preventDefault(); }} > Create new folder Create new folder
); } _renderRenameDialog() { return ( 0} onOpenChange={(open) => (open ? {} : this.setState({ renameFile: "" }))} >
{ const newName = (e.target as any).elements.name.value; this._onFileRename(this.state.renameFile, newName); e.preventDefault(); }} > Rename {this.state.renameFile} Rename {this.state.renameFile}
); } _renderErrorDialog() { return ( 0} onOpenChange={(open) => (open ? {} : this.setState({ errorText: "" }))} > Error Error: {this.state.errorText}
{this.state.errorText}
); } _renderRemoveDialog() { return ( 0} onOpenChange={(open) => (open ? {} : this.setState({ removeFile: "" }))} > Delete {this.state.removeFile}? Delete {this.state.removeFile}
Are you sure you want to delete{" "} {makeRelativePathWithState(this.state, this.state.removeFile)}
); } _renderResetDialog() { return ( this.setState({ resetFilesys: open })} > Reset filesystem Reset filesystem
Are you sure you want to reset the filesystem?
); } _renderBreadcrumbElements() { const elements = generateBreadcrumbElements(this.state.path); return elements.map((e, index) => { if (index == this.state.path.length) { return ( {e.node} ); } const navigate = () => this.setState({ path: e.targetPath }); return ( <> {e.node} ); }); } _renderBreadCrumb() { return ( {this._renderBreadcrumbElements()} ); } render() { const elements = getFolderElements(this.props.filesystem, this.state.path); return ( <> {this._renderCreateFolderDialog()} {this._renderRenameDialog()} {this._renderErrorDialog()} {this._renderRemoveDialog()} {this._renderResetDialog()}
{this._renderBreadCrumb()}
{({ getRootProps, getInputProps }) => (
this.setState({ createFolder: true }) } removeElementHandler={(e) => this.setState({ removeFile: e.name }) } renameElementHandler={(e) => this.setState({ renameFile: e.name }) } addFilesHandler={this._onAddFiles} />
)}
); } }