import React from "react"; import { Folder, FolderElement, FolderElementType, trimFilename, } 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"; import { parsePeIcon } from "./pe-icon-parser"; export interface FilesystemExplorerProps { filesystem: Filesystem; runFile: (file: string) => void; resetFilesys: () => void; path: string[]; iconCache: Map; } 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(); }); } function getPeIcon( filesystem: Filesystem, file: string, cache: Map, ) { if (!file || !file.endsWith(".exe")) { return null; } const cachedValue = cache.get(file); if (cachedValue) { return cachedValue; } const data = filesystem.readFile(file); const icon = parsePeIcon(data); cache.set(file, icon); return icon; } interface BreadcrumbElement { node: React.ReactNode; targetPath: string[]; } function isGoodPath(path: any) { return typeof path === "string" && path.length > 0; } function trimLeadingSlash(path: string) { if (path.startsWith("/")) { return path.substring(1); } return path; } function getFileName(file: File) { const fileObj = file as any; const properties = ["relativePath", "webkitRelativePath", "name"]; for (let i = 0; i < properties.length; ++i) { const prop = properties[i]; if (prop in fileObj) { const relativePath = fileObj[prop]; if (isGoodPath(relativePath)) { return trimLeadingSlash(relativePath); } } } return file.name; } 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; } function downloadData( data: Uint8Array, filename: string, mimeType: string = "application/octet-stream", ) { const buffer = data.buffer.slice( data.byteOffset, data.byteOffset + data.byteLength, ) as ArrayBuffer; const blob = new Blob([buffer], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); } 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) { newFile = newFile.toLowerCase(); if (newFile == file) { this.setState({ renameFile: "" }); return; } if (!this._validateName(newFile)) { return; } const oldPath = makeFullPathWithState(this.state, file); const newPath = makeFullPathWithState(this.state, newFile); this.setState({ renameFile: "" }); this._removeFromCache(file); this._removeFromCache(newFile); await this.props.filesystem.rename(oldPath, newPath); this.forceUpdate(); } async _onAddFiles() { const files = await selectFiles(); await this._uploadFiles(files); } _validateName(name: string) { if (name.length == 0) { return false; } if (name.includes("/") || name.includes("\\")) { this._showError("Folder must not contain special characters"); return false; } if (this.state.path.length == 0 && name.length > 1) { this._showError("Drives must be a single letter"); return false; } return true; } async _onFolderCreate(name: string) { name = name.toLowerCase(); if (!this._validateName(name)) { return; } this.setState({ createFolder: false }); 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) => { const name = getFileName(f.file); return { name: makeFullPathWithState(this.state, name.toLowerCase()), data: f.data, }; }); fileData.forEach((d) => { this._removeFromCache(d.name); }); 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 {trimFilename(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 {trimFilename(this.state.removeFile)}? Delete {this.state.removeFile}
Are you sure you want to delete{" "} {makeWindowsPathWithState(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()} ); } _removeFromCache(file: string) { this.props.iconCache.delete(file); } _downloadFile(file: string) { const fullPath = makeFullPathWithState(this.state, file); const data = this.props.filesystem.readFile(fullPath); downloadData(data, file); } 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 }) } downloadElementHandler={(e) => this._downloadFile(e.name)} addFilesHandler={this._onAddFiles} iconReader={(e) => getPeIcon( this.props.filesystem, makeFullPathWithState(this.state, e.name), this.props.iconCache, ) } />
)}
); } }