starboard-python
Version:
Python cells for Starboard Notebook
298 lines (270 loc) • 9.29 kB
text/typescript
// 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("/");
}