From b7333b403d2238658a83c1ca7ca64ac14bbe8f39 Mon Sep 17 00:00:00 2001 From: momo5502 Date: Sun, 20 Jul 2025 16:15:02 +0200 Subject: [PATCH] Prepare support for sharing files --- page/src/App.tsx | 11 +++- page/src/download.ts | 86 +++++++++++++++++++++++++++++ page/src/filesystem.ts | 80 +++++++++------------------ page/src/playground.tsx | 119 +++++++++++++++++++++++++++++++--------- 4 files changed, 216 insertions(+), 80 deletions(-) create mode 100644 page/src/download.ts diff --git a/page/src/App.tsx b/page/src/App.tsx index ce332ae1..d9a4510a 100644 --- a/page/src/App.tsx +++ b/page/src/App.tsx @@ -1,9 +1,11 @@ import { ThemeProvider } from "@/components/theme-provider"; import { TooltipProvider } from "@/components/ui/tooltip"; import { HashRouter, Route, Routes, Navigate } from "react-router-dom"; -import { Playground } from "./playground"; +import { Playground, PlaygroundFile, storeEmulateData } from "./playground"; import { LandingPage } from "./landing-page"; +import { useParams } from "react-router-dom"; + import "@fontsource/inter/100.css"; import "@fontsource/inter/200.css"; import "@fontsource/inter/300.css"; @@ -16,6 +18,12 @@ import "@fontsource/inter/900.css"; import "./App.css"; +function EmulateFile() { + const { encodedData } = useParams(); + storeEmulateData(encodedData); + return ; +} + function App() { return ( @@ -25,6 +33,7 @@ function App() { } /> } /> } /> + } /> diff --git a/page/src/download.ts b/page/src/download.ts new file mode 100644 index 00000000..f2df1b45 --- /dev/null +++ b/page/src/download.ts @@ -0,0 +1,86 @@ +export type DownloadProgressHandler = ( + receivedBytes: number, + totalBytes: number, +) => void; + +export type DownloadPercentHandler = (percent: number) => void; + +export function makePercentHandler( + handler: DownloadPercentHandler, +): DownloadProgressHandler { + const progress = { + tracked: 0, + }; + + return (current, total) => { + if (total == 0) { + return; + } + + const percent = Math.floor((current * 100) / total); + const sanePercent = Math.max(Math.min(percent, 100), 0); + + if (sanePercent + 1 > progress.tracked) { + progress.tracked = sanePercent + 1; + handler(sanePercent); + } + }; +} + +export function downloadBinaryFile( + file: string, + progressCallback: DownloadProgressHandler, +) { + return fetch(file, { + method: "GET", + headers: { + "Content-Type": "application/octet-stream", + }, + }).then((response) => { + const maybeReader = response.body?.getReader(); + if (!maybeReader) { + throw new Error("Bad reader"); + } + + const reader = maybeReader; + + const contentLength = parseInt( + response.headers?.get("Content-Length") || "0", + ); + + let receivedLength = 0; + let chunks: Uint8Array[] = []; + + function processData( + res: ReadableStreamReadResult>, + ): Promise { + if (res.value) { + chunks.push(res.value); + receivedLength += res.value.length; + } + + progressCallback(receivedLength, contentLength); + + if (!res.done) { + return reader.read().then(processData); + } + const chunksAll = new Uint8Array(receivedLength); + let position = 0; + for (const chunk of chunks) { + chunksAll.set(new Uint8Array(chunk), position); + position += chunk.length; + } + + return Promise.resolve(chunksAll.buffer); + } + + return reader.read().then(processData); + }); +} + +export function downloadBinaryFilePercent( + file: string, + progressCallback: DownloadPercentHandler, +) { + return downloadBinaryFile(file, makePercentHandler(progressCallback)); +} diff --git a/page/src/filesystem.ts b/page/src/filesystem.ts index 1a7e4390..70fef39d 100644 --- a/page/src/filesystem.ts +++ b/page/src/filesystem.ts @@ -1,62 +1,14 @@ +import { downloadBinaryFilePercent, DownloadPercentHandler } from "./download"; import { parseZipFile, ProgressHandler } from "./zip-file"; import idbfsModule, { MainModule } from "@irori/idbfs"; -type DownloadProgressHandler = ( - receivedBytes: number, - totalBytes: number, -) => void; - -function fetchFilesystemZip(progressCallback: DownloadProgressHandler) { - return fetch("./root.zip", { - method: "GET", - headers: { - "Content-Type": "application/octet-stream", - }, - }).then((response) => { - const maybeReader = response.body?.getReader(); - if (!maybeReader) { - throw new Error("Bad reader"); - } - - const reader = maybeReader; - - const contentLength = parseInt( - response.headers?.get("Content-Length") || "0", - ); - - let receivedLength = 0; - let chunks: Uint8Array[] = []; - - function processData( - res: ReadableStreamReadResult>, - ): Promise { - if (res.value) { - chunks.push(res.value); - receivedLength += res.value.length; - } - - progressCallback(receivedLength, contentLength); - - if (!res.done) { - return reader.read().then(processData); - } - const chunksAll = new Uint8Array(receivedLength); - let position = 0; - for (const chunk of chunks) { - chunksAll.set(new Uint8Array(chunk), position); - position += chunk.length; - } - - return Promise.resolve(chunksAll.buffer); - } - - return reader.read().then(processData); - }); +function fetchFilesystemZip(progressCallback: DownloadPercentHandler) { + return downloadBinaryFilePercent("./root.zip", progressCallback); } async function fetchFilesystem( progressHandler: ProgressHandler, - downloadProgressHandler: DownloadProgressHandler, + downloadProgressHandler: DownloadPercentHandler, ) { const filesys = await fetchFilesystemZip(downloadProgressHandler); return await parseZipFile(filesys, progressHandler); @@ -74,6 +26,28 @@ function synchronizeIDBFS(idbfs: MainModule, populate: boolean) { }); } +const filesystemPrefix = "/root/filesys/"; + +export function internalToWindowsPath(internalPath: string): string { + if ( + !internalPath.startsWith(filesystemPrefix) || + internalPath.length <= filesystemPrefix.length + ) { + throw new Error("Invalid path"); + } + + const winPath = internalPath.substring(filesystemPrefix.length); + return `${winPath[0]}:${winPath.substring(1)}`; +} + +export function windowsToInternalPath(windowsPath: string): string { + if (windowsPath.length < 2 || windowsPath[1] != ":") { + throw new Error("Invalid path"); + } + + return `${filesystemPrefix}${windowsPath[0]}${windowsPath.substring(2)}`; +} + async function initializeIDBFS() { const idbfs = await idbfsModule(); @@ -210,7 +184,7 @@ export class Filesystem { export async function setupFilesystem( progressHandler: ProgressHandler, - downloadProgressHandler: DownloadProgressHandler, + downloadProgressHandler: DownloadPercentHandler, ) { const idbfs = await initializeIDBFS(); const fs = new Filesystem(idbfs); diff --git a/page/src/playground.tsx b/page/src/playground.tsx index 89f19db1..40bec190 100644 --- a/page/src/playground.tsx +++ b/page/src/playground.tsx @@ -3,7 +3,11 @@ import React from "react"; import { Output } from "@/components/output"; import { Emulator, EmulationState, isFinalState } from "./emulator"; -import { Filesystem, setupFilesystem } from "./filesystem"; +import { + Filesystem, + setupFilesystem, + windowsToInternalPath, +} from "./filesystem"; import { memory64 } from "wasm-feature-detect"; @@ -39,11 +43,17 @@ import { } from "@/components/ui/drawer"; import { FilesystemExplorer } from "./filesystem-explorer"; import { EmulationStatus } from "./emulator"; -import { TextTooltip } from "./components/text-tooltip"; import { EmulationSummary } from "./components/emulation-summary"; +import { downloadBinaryFilePercent } from "./download"; -interface PlaygroundProps {} -interface PlaygroundState { +export interface PlaygroundFile { + file: string; + storage: string; +} + +export interface PlaygroundProps {} + +export interface PlaygroundState { settings: Settings; filesystemPromise?: Promise; filesystem?: Filesystem; @@ -52,28 +62,57 @@ interface PlaygroundState { application?: string; drawerOpen: boolean; allowWasm64: boolean; + file?: PlaygroundFile; } -function makePercentHandler( - handler: (percent: number) => void, -): (current: number, total: number) => void { - const progress = { - tracked: 0, - }; +function decodeFileData(data: string | null): PlaygroundFile | undefined { + if (!data) { + return undefined; + } - return (current, total) => { - if (total == 0) { - return; - } + try { + const jsonData = JSON.parse(atob(data)); - const percent = Math.floor((current * 100) / total); - const sanePercent = Math.max(Math.min(percent, 100), 0); + return { + file: jsonData.file, + storage: jsonData.storage, + }; + } catch (e) { + console.log(e); + } - if (sanePercent + 1 > progress.tracked) { - progress.tracked = sanePercent + 1; - handler(sanePercent); - } - }; + return undefined; +} + +interface GlobalThisExt { + emulateCache?: string | null; +} + +function getGlobalThis() { + return globalThis as GlobalThisExt; +} + +export function storeEmulateData(data?: string) { + getGlobalThis().emulateCache = undefined; + + if (data) { + localStorage.setItem("emulate", data); + } else { + localStorage.removeItem("emulate"); + } +} + +function getEmulateData() { + const gt = getGlobalThis(); + if (gt.emulateCache) { + return gt.emulateCache; + } + + const emulateData = localStorage.getItem("emulate"); + localStorage.removeItem("emulate"); + + gt.emulateCache = emulateData; + return emulateData; } export class Playground extends React.Component< @@ -90,13 +129,14 @@ export class Playground extends React.Component< this.start = this.start.bind(this); this.resetFilesys = this.resetFilesys.bind(this); - this.createEmulator = this.createEmulator.bind(this); + this.startEmulator = this.startEmulator.bind(this); this.toggleEmulatorState = this.toggleEmulatorState.bind(this); this.state = { settings: loadSettings(), drawerOpen: false, allowWasm64: false, + file: decodeFileData(getEmulateData()), }; } @@ -104,6 +144,10 @@ export class Playground extends React.Component< memory64().then((allowWasm64) => { this.setState({ allowWasm64 }); }); + + if (this.state.file) { + this.emulateRemoteFile(this.state.file); + } } async resetFilesys() { @@ -151,9 +195,9 @@ export class Playground extends React.Component< (current, total, file) => { this.logLine(`Processing filesystem (${current}/${total}): ${file}`); }, - makePercentHandler((percent) => { + (percent) => { this.logLine(`Downloading filesystem: ${percent}%`); - }), + }, ).then(resolve); }); @@ -167,6 +211,29 @@ export class Playground extends React.Component< this.setState({ drawerOpen }); } + async downloadFileToFilesystem(file: PlaygroundFile) { + const fs = await this.initFilesys(); + + const fileData = await downloadBinaryFilePercent( + file.storage, + (percent) => { + this.logLine(`Downloading binary: ${percent}%`); + }, + ); + + await fs.storeFiles([ + { + name: windowsToInternalPath(file.file), + data: fileData, + }, + ]); + } + + async emulateRemoteFile(file: PlaygroundFile) { + await this.downloadFileToFilesystem(file); + await this.startEmulator(file.file); + } + async start() { await this.initFilesys(); this.setDrawerOpen(true); @@ -195,7 +262,7 @@ export class Playground extends React.Component< } } - async createEmulator(userFile: string) { + async startEmulator(userFile: string) { this.state.emulator?.stop(); this.output.current?.clear(); @@ -316,7 +383,7 @@ export class Playground extends React.Component<