Skip to content

Commit

Permalink
Add mountNodeFS (#4561)
Browse files Browse the repository at this point in the history
This is a helper function for mounting native directories in node, analogous to mountNativeFS.
  • Loading branch information
hoodmane committed Feb 26, 2024
1 parent e643019 commit 0fc565f
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 22 deletions.
4 changes: 4 additions & 0 deletions docs/project/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ myst:
it fails.
{pr}`4559`

- {{ Enhancement }} Added a new API `pyodide.mountNodeFS` which mounts a host
directory into the Pyodide file system when running in node.
{pr}`4561`

### Packages

- New Packages: `cysignals`, `ppl`, `pplpy` {pr}`4407`, `flint`, `python-flint` {pr}`4410`,
Expand Down
79 changes: 58 additions & 21 deletions src/js/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { CanvasInterface, canvas } from "./canvas";

import { PackageData, loadPackage, loadedPackages } from "./load-package";
import { type PyProxy, type PyDict } from "generated/pyproxy";
import { loadBinaryFile } from "./compat";
import { loadBinaryFile, nodeFSMod } from "./compat";
import { version } from "./version";
import { setStdin, setStdout, setStderr } from "./streams";
import { TypedArray } from "./types";
import { IN_NODE } from "./environments";

// Exported for micropip
API.loadBinaryFile = loadBinaryFile;
Expand Down Expand Up @@ -50,6 +51,23 @@ API.saveState = () => API.pyodide_py._state.save_state();
/** @private */
API.restoreState = (state: any) => API.pyodide_py._state.restore_state(state);

function ensureMountPathExists(path: string): void {
Module.FS.mkdirTree(path);
const { node } = Module.FS.lookupPath(path, {
follow_mount: false,
});

if (FS.isMountpoint(node)) {
throw new Error(`path '${path}' is already a file system mount point`);
}
if (!FS.isDir(node.mode)) {
throw new Error(`path '${path}' points to a file not a directory`);
}
for (const _ in node.contents) {
throw new Error(`directory '${path}' is not empty`);
}
}

/**
* Why is this a class rather than an object?
* 1. It causes documentation items to be created for the entries so we can copy
Expand Down Expand Up @@ -482,12 +500,15 @@ export class PyodideAPI {

/**
* Mounts a :js:class:`FileSystemDirectoryHandle` into the target directory.
* Currently it's only possible to acquire a
* :js:class:`FileSystemDirectoryHandle` in Chrome.
*
* @param path The absolute path in the Emscripten file system to mount the
* native directory. If the directory does not exist, it will be created. If it
* does exist, it must be empty.
* @param fileSystemHandle A handle returned by :js:func:`navigator.storage.getDirectory() <getDirectory>`
* or :js:func:`window.showDirectoryPicker() <showDirectoryPicker>`.
* native directory. If the directory does not exist, it will be created. If
* it does exist, it must be empty.
* @param fileSystemHandle A handle returned by
* :js:func:`navigator.storage.getDirectory() <getDirectory>` or
* :js:func:`window.showDirectoryPicker() <showDirectoryPicker>`.
*/
static async mountNativeFS(
path: string,
Expand All @@ -500,25 +521,11 @@ export class PyodideAPI {
`Expected argument 'fileSystemHandle' to be a FileSystemDirectoryHandle`,
);
}

Module.FS.mkdirTree(path);
const { node } = Module.FS.lookupPath(path, {
follow_mount: false,
});

if (FS.isMountpoint(node)) {
throw new Error(`path '${path}' is already a file system mount point`);
}
if (!FS.isDir(node.mode)) {
throw new Error(`path '${path}' points to a file not a directory`);
}
for (const _ in node.contents) {
throw new Error(`directory '${path}' is not empty`);
}
ensureMountPathExists(path);

Module.FS.mount(
Module.FS.filesystems.NATIVEFS_ASYNC,
{ fileSystemHandle: fileSystemHandle },
{ fileSystemHandle },
path,
);

Expand All @@ -532,6 +539,36 @@ export class PyodideAPI {
};
}

/**
* Mounts a host directory into Pyodide file system. Only works in node.
*
* @param emscriptenPath The absolute path in the Emscripten file system to
* mount the native directory. If the directory does not exist, it will be
* created. If it does exist, it must be empty.
* @param hostPath The host path to mount. It must be a directory that exists.
*/
static mountNodeFS(emscriptenPath: string, hostPath: string): void {
if (!IN_NODE) {
throw new Error("mountNodeFS only works in Node");
}
ensureMountPathExists(emscriptenPath);
let stat;
try {
stat = nodeFSMod.lstatSync(hostPath);
} catch (e) {
throw new Error(`hostPath '${hostPath}' does not exist`);
}
if (!stat.isDirectory()) {
throw new Error(`hostPath '${hostPath}' is not a directory`);
}

Module.FS.mount(
Module.FS.filesystems.NODEFS,
{ root: hostPath },
emscriptenPath,
);
}

/**
* Tell Pyodide about Comlink.
* Necessary to enable importing Comlink proxies into Python.
Expand Down
47 changes: 46 additions & 1 deletion src/tests/test_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
from pytest_pyodide import run_in_pyodide

from conftest import only_chrome
from conftest import only_chrome, only_node


@pytest.mark.skip_refcount_check
Expand Down Expand Up @@ -314,6 +314,51 @@ def test_nativefs_errors(selenium):
)


@only_node
def test_mount_nodefs(selenium):
selenium.run_js(
"""
pyodide.mountNodeFS("/mnt1/nodefs", ".");
assertThrows(
() => pyodide.mountNodeFS("/mnt1/nodefs", "."),
"Error",
"path '/mnt1/nodefs' is already a file system mount point"
);
assertThrows(
() =>
pyodide.mountNodeFS(
"/mnt2/nodefs",
"/thispath/does-not/exist/ihope"
),
"Error",
"hostPath '/thispath/does-not/exist/ihope' does not exist"
);
const os = require("os");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const tmpdir = path.join(os.tmpdir(), crypto.randomUUID());
fs.mkdirSync(tmpdir);
const apath = path.join(tmpdir, "a");
fs.writeFileSync(apath, "xyz");
pyodide.mountNodeFS("/mnt3/nodefs", tmpdir);
assert(
() =>
pyodide.FS.readFile("/mnt3/nodefs/a", { encoding: "utf8" }) ===
"xyz"
);
assertThrows(
() => pyodide.mountNodeFS("/mnt4/nodefs", apath),
"Error",
`hostPath '${apath}' is not a directory`
);
"""
)


@pytest.fixture
def browser(selenium):
return selenium.browser
Expand Down

0 comments on commit 0fc565f

Please sign in to comment.