UNPKG

starboard-python

Version:
298 lines (270 loc) 9.29 kB
// see // https://github.com/jvilk/BrowserFS/blob/master/src/generic/emscripten_fs.ts // https://github.com/emscripten-core/emscripten/blob/main/src/library_nodefs.js // https://github.com/emscripten-core/emscripten/blob/main/src/library_memfs.js // https://github.com/emscripten-core/emscripten/blob/main/src/library_workerfs.js // https://github.com/curiousdannii/emglken/blob/master/src/emglkenfs.js // TODO: Use the types from starboard? type SyncResult<T, E = Error> = | { ok: true; data: T; } | { ok: false; status: number; error: E; detail?: string; }; export interface NotebookFilesystemSync { /** * Get a file or directory at a given path. * @returns The contents of the file. `null` corresponds to a directory */ get(opts: { path: string }): SyncResult<string | null>; /** * Creates or replaces a file or directory at a given path. * @param opts.value The contents of the file. `null` corresponds to a directory */ put(opts: { path: string; value: string | null }): SyncResult<undefined>; /** * Deletes a file or directory at a given path */ delete(opts: { path: string }): SyncResult<undefined>; /** * Move a file or directory to a new path. Can be used for renaming */ move(opts: { path: string; newPath: string }): SyncResult<undefined>; /** * List the files in a directory */ listDirectory(opts: { path: string }): SyncResult<string[]>; } interface EMFSNode { name: string; mode: number; parent: EMFSNode; mount: { opts: { root: string } }; id: any; timestamp: any; stream_ops: any; node_ops: any; } interface EMFSStream { node: EMFSNode; position: number; fileData?: Uint8Array; } const DIR_MODE = 16895; // 040777 const FILE_MODE = 33206; // 100666 const SEEK_CUR = 1; const SEEK_END = 2; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8"); export class EMFS { FS: any; ERRNO_CODES: any; CUSTOM_FS: NotebookFilesystemSync; node_ops = {} as any; stream_ops = {} as any; constructor(FS: any, ERRNO_CODES: any, CUSTOM_FS: NotebookFilesystemSync) { this.FS = FS; this.ERRNO_CODES = ERRNO_CODES; this.CUSTOM_FS = CUSTOM_FS; this.node_ops.getattr = (node: EMFSNode) => { return { dev: 1, ino: node.id, mode: node.mode, nlink: 1, uid: 0, gid: 0, rdev: undefined, size: 0, atime: new Date(node.timestamp), mtime: new Date(node.timestamp), ctime: new Date(node.timestamp), blksize: 4096, blocks: 0, }; }; this.node_ops.setattr = (node: EMFSNode, attr: any) => { // Doesn't really do anything if (attr.mode !== undefined) { node.mode = attr.mode; } if (attr.timestamp !== undefined) { node.timestamp = attr.timestamp; } }; this.node_ops.lookup = (parent: EMFSNode, name: string) => { const path = realPath(parent, name); const result = this.CUSTOM_FS.get({ path }); if (!result.ok) { // I wish Javascript had inner exceptions throw this.FS.genericErrors[this.ERRNO_CODES["ENOENT"]]; } return this.createNode(parent, name, result.data === null ? DIR_MODE : FILE_MODE); }; this.node_ops.mknod = (parent: EMFSNode, name: string, mode: number, dev?: any) => { const node = this.createNode(parent, name, mode, dev); const path = realPath(node); if (this.FS.isDir(node.mode)) { this.convertSyncResult(this.CUSTOM_FS.put({ path, value: null })); } else { this.convertSyncResult(this.CUSTOM_FS.put({ path, value: "" })); } return node; }; this.node_ops.rename = (oldNode: EMFSNode, newDir: EMFSNode, newName: string) => { const oldPath = realPath(oldNode); const newPath = realPath(newDir, newName); this.convertSyncResult(this.CUSTOM_FS.move({ path: oldPath, newPath: newPath })); oldNode.name = newName; }; this.node_ops.unlink = (parent: EMFSNode, name: string) => { const path = realPath(parent, name); this.convertSyncResult(this.CUSTOM_FS.delete({ path })); }; this.node_ops.rmdir = (parent: EMFSNode, name: string) => { const path = realPath(parent, name); this.convertSyncResult(this.CUSTOM_FS.delete({ path })); }; this.node_ops.readdir = (node: EMFSNode) => { const path = realPath(node); let result = this.convertSyncResult(this.CUSTOM_FS.listDirectory({ path })); if (!result.includes(".")) { result.push("."); } if (!result.includes("..")) { result.push(".."); } return result; }; this.node_ops.symlink = (parent: EMFSNode, newName: string, oldPath: string) => { throw new FS.ErrnoError(this.ERRNO_CODES["EPERM"]); }; this.node_ops.readlink = (node: EMFSNode) => { throw new FS.ErrnoError(this.ERRNO_CODES["EPERM"]); }; this.stream_ops.open = (stream: EMFSStream) => { const path = realPath(stream.node); if (FS.isFile(stream.node.mode)) { const result = this.convertSyncResult(this.CUSTOM_FS.get({ path })); if (result === null) { return; } stream.fileData = encoder.encode(result); } }; this.stream_ops.close = (stream: EMFSStream) => { const path = realPath(stream.node); if (FS.isFile(stream.node.mode) && stream.fileData) { const text = decoder.decode(stream.fileData); stream.fileData = undefined; this.convertSyncResult(this.CUSTOM_FS.put({ path, value: text })); } }; this.stream_ops.read = ( stream: EMFSStream, buffer: Uint8Array, offset: number, length: number, position: number ) => { if (length <= 0) return 0; const size = Math.min((stream.fileData?.length ?? 0) - position, length); try { buffer.set(stream.fileData!.subarray(position, position + size), offset); } catch (e) { throw new FS.ErrnoError(this.ERRNO_CODES["EPERM"]); } return size; }; this.stream_ops.write = ( stream: EMFSStream, buffer: Uint8Array, offset: number, length: number, position: number ) => { if (length <= 0) return 0; stream.node.timestamp = Date.now(); try { if (position + length > (stream.fileData?.length ?? 0)) { // Resize // If this gets called very often, maybe resizing it by some multiple of its current size would be a better idea const oldData = stream.fileData ?? new Uint8Array(); stream.fileData = new Uint8Array(position + length); stream.fileData.set(oldData); } // Write stream.fileData!.set(buffer.subarray(offset, offset + length), position); return length; } catch (e) { throw new FS.ErrnoError(this.ERRNO_CODES["EPERM"]); } }; this.stream_ops.llseek = (stream: EMFSStream, offset: number, whence: number) => { let position = offset; if (whence === SEEK_CUR) { position += stream.position; } else if (whence === SEEK_END) { if (this.FS.isFile(stream.node.mode)) { try { // Not sure, but let's see position += stream.fileData!.length; } catch (e) { throw new FS.ErrnoError(this.ERRNO_CODES["EPERM"]); } } } if (position < 0) { throw new FS.ErrnoError(this.ERRNO_CODES["EINVAL"]); } return position; }; } mount(mount: { opts: { root: string } }) { return this.createNode(null, "/", DIR_MODE, 0); } createNode(parent: EMFSNode | null, name: string, mode: number, dev?: any) { if (!this.FS.isDir(mode) && !this.FS.isFile(mode)) { throw new this.FS.ErrnoError(this.ERRNO_CODES["EINVAL"]); } let node = this.FS.createNode(parent, name, mode); node.node_ops = this.node_ops; node.stream_ops = this.stream_ops; return node; } private convertSyncResult<T, E>(result: SyncResult<T, E>): T { if (result.ok) { return result.data; } else { let error; if (result.status === 404) { error = new this.FS.ErrnoError(this.ERRNO_CODES["ENOENT"]); } else if (result.status === 400) { error = new this.FS.ErrnoError(this.ERRNO_CODES["EINVAL"]); } else { error = new this.FS.ErrnoError(this.ERRNO_CODES["EPERM"]); } // I'm so looking forward to https://github.com/tc39/proposal-error-cause error.cause = result.error; throw error; } } } function realPath(node: EMFSNode, fileName?: string) { const parts = []; while (node.parent !== node) { parts.push(node.name); node = node.parent; } parts.push(node.mount.opts.root); parts.reverse(); if (fileName !== undefined && fileName !== null) { parts.push(fileName); } return parts.join("/"); }