Skip to content

pyscript.fs

This module provides an API for mounting directories from the user's local filesystem into the browser's virtual filesystem. This means Python code, running in the browser, can read and write files on the user's local machine.

Warning

This API only works in Chromium-based browsers (Chrome, Edge, Vivaldi, Brave, etc.) that support the File System Access API.

The module maintains a mounted dictionary that tracks all currently mounted paths and their associated filesystem handles.

from pyscript import fs, document, when


# Mount a local directory to the `/local` mount point in the browser's
# virtual filesystem (may prompt user for permission).
await fs.mount("/local")

# Alternatively, mount on a button click event. This is important because
# if the call to `fs.mount` happens after a click or other transient event,
# the confirmation dialog will not be shown.
@when("click", "#mount-button")
async def handler(event):
    await fs.mount("/another_dir")

# Work with files in the mounted directory as usual.
with open("/local/example.txt", "w") as f:
    f.write("Hello from PyScript!")

# Ensure changes are written to local filesystem.
await fs.sync("/local")

# Clean up when done.
await fs.unmount("/local")

mounted = {} module-attribute

Global dictionary tracking mounted paths and their filesystem handles.

mount(path, mode='readwrite', root='', id='pyscript') async

Mount a directory from the local filesystem to the virtual filesystem at the specified path mount point. The mode can be "readwrite" or "read" to specify access level. The root parameter provides a hint for the file picker starting location. The id parameter allows multiple distinct mounts at the same path.

On first use, the browser will prompt the user to select a directory and grant permission.

from pyscript import fs


# Basic mount with default settings.
await fs.mount("/local")

# Mount with read-only access.
await fs.mount("/readonly", mode="read")

# Mount with a hint to start in Downloads folder.
await fs.mount("/downloads", root="downloads")

# Mount with a custom ID to track different directories.
await fs.mount("/project", id="my-project")

If called during a user interaction (like a button click), the permission dialog may be skipped if permission was previously granted.

Source code in pyscript/fs.py
async def mount(path, mode="readwrite", root="", id="pyscript"):
    """
    Mount a directory from the local filesystem to the virtual filesystem
    at the specified `path` mount point. The `mode` can be "readwrite" or
    "read" to specify access level. The `root` parameter provides a hint
    for the file picker starting location. The `id` parameter allows multiple
    distinct mounts at the same path.

    On first use, the browser will prompt the user to select a directory
    and grant permission.

    ```python
    from pyscript import fs


    # Basic mount with default settings.
    await fs.mount("/local")

    # Mount with read-only access.
    await fs.mount("/readonly", mode="read")

    # Mount with a hint to start in Downloads folder.
    await fs.mount("/downloads", root="downloads")

    # Mount with a custom ID to track different directories.
    await fs.mount("/project", id="my-project")
    ```

    If called during a user interaction (like a button click), the
    permission dialog may be skipped if permission was previously granted.
    """
    js.console.warn("experimental pyscript.fs ⚠️")

    # Check if path is already mounted with a different ID.
    mount_key = f"{path}@{id}"
    if path in mounted:
        # Path already mounted - check if it's the same ID.
        for existing_key in mounted.keys():
            if existing_key.startswith(f"{path}@") and existing_key != mount_key:
                raise ValueError(
                    f"Path '{path}' is already mounted with a different ID. "
                    f"Unmount it first or use a different path."
                )

    details = None
    handler = None

    options = {"id": id, "mode": mode}
    if root != "":
        options["startIn"] = root

    if RUNNING_IN_WORKER:
        fs_handler = sync_with_worker.storeFSHandler(mount_key, to_js(options))

        # Handle both async and SharedArrayBuffer use cases.
        if isinstance(fs_handler, bool):
            success = fs_handler
        else:
            success = await fs_handler

        if success:
            idbm = IDBMap.new(_fs.NAMESPACE)
            details = await idbm.get(mount_key)
            handler = await _check_permission(details)
            if handler is None:
                # Force await in either async or sync scenario.
                await js.Promise.resolve(sync_with_worker.getFSHandler(details.options))
                handler = details.handler
        else:
            raise RuntimeError(_fs.ERROR)

    else:
        success = await _fs.idb.has(mount_key)

        if success:
            details = await _fs.idb.get(mount_key)
            handler = await _check_permission(details)
            if handler is None:
                handler = await _fs.getFileSystemDirectoryHandle(details.options)
        else:
            js_options = to_js(options)
            handler = await _fs.getFileSystemDirectoryHandle(js_options)
            details = {"handler": handler, "options": js_options}
            await _fs.idb.set(mount_key, to_js(details))

    mounted[path] = await interpreter.mountNativeFS(path, handler)

sync(path) async

Synchronise the virtual and local filesystems for a mounted path.

This ensures all changes made in the browser's virtual filesystem are written to the user's local filesystem, and vice versa.

from pyscript import fs


await fs.mount("/local")

# Make changes to files.
with open("/local/data.txt", "w") as f:
    f.write("Important data")

# Ensure changes are written to local disk.
await fs.sync("/local")

This is automatically called by unmount(), but you may want to call it explicitly to ensure data persistence at specific points.

Source code in pyscript/fs.py
async def sync(path):
    """
    Synchronise the virtual and local filesystems for a mounted `path`.

    This ensures all changes made in the browser's virtual filesystem are
    written to the user's local filesystem, and vice versa.

    ```python
    from pyscript import fs


    await fs.mount("/local")

    # Make changes to files.
    with open("/local/data.txt", "w") as f:
        f.write("Important data")

    # Ensure changes are written to local disk.
    await fs.sync("/local")
    ```

    This is automatically called by unmount(), but you may want to call
    it explicitly to ensure data persistence at specific points.
    """
    if path not in mounted:
        raise KeyError(
            f"Path '{path}' is not mounted. " f"Use fs.mount() to mount it first."
        )
    await mounted[path].syncfs()

unmount(path) async

Unmount a directory, specified by path, from the virtual filesystem.

This synchronises any pending changes and then removes the mount point, freeing up memory. The path can be reused for mounting a different directory.

from pyscript import fs


await fs.mount("/local")
# ... work with files ...
await fs.unmount("/local")

# Path can now be reused.
await fs.mount("/local", id="different-folder")

This automatically calls sync() before unmounting to ensure no data is lost.

Source code in pyscript/fs.py
async def unmount(path):
    """
    Unmount a directory, specified by `path`, from the virtual filesystem.

    This synchronises any pending changes and then removes the mount point,
    freeing up memory. The `path` can be reused for mounting a different
    directory.

    ```python
    from pyscript import fs


    await fs.mount("/local")
    # ... work with files ...
    await fs.unmount("/local")

    # Path can now be reused.
    await fs.mount("/local", id="different-folder")
    ```

    This automatically calls `sync()` before unmounting to ensure no data
    is lost.
    """
    if path not in mounted:
        raise KeyError(f"Path '{path}' is not mounted. Cannot unmount.")

    await sync(path)
    interpreter._module.FS.unmount(path)
    del mounted[path]

revoke(path, id='pyscript') async

Revoke filesystem access permission and unmount for a given path and id combination.

This removes the stored permission for accessing the user's local filesystem at the specified path and ID. Unlike unmount(), which only removes the mount point, revoke() also clears the permission so the user will be prompted again on next mount.

from pyscript import fs


await fs.mount("/local", id="my-app")
# ... work with files ...

# Revoke permission (user will be prompted again next time).
revoked = await fs.revoke("/local", id="my-app")

if revoked:
    print("Permission revoked successfully")

After revoking, the user will need to grant permission again and select a directory when mount() is called next time.

Source code in pyscript/fs.py
async def revoke(path, id="pyscript"):
    """
    Revoke filesystem access permission and unmount for a given
    `path` and `id` combination.

    This removes the stored permission for accessing the user's local
    filesystem at the specified path and ID. Unlike `unmount()`, which only
    removes the mount point, `revoke()` also clears the permission so the
    user will be prompted again on next mount.

    ```python
    from pyscript import fs


    await fs.mount("/local", id="my-app")
    # ... work with files ...

    # Revoke permission (user will be prompted again next time).
    revoked = await fs.revoke("/local", id="my-app")

    if revoked:
        print("Permission revoked successfully")
    ```

    After revoking, the user will need to grant permission again and
    select a directory when `mount()` is called next time.
    """
    mount_key = f"{path}@{id}"

    if RUNNING_IN_WORKER:
        handler_exists = sync_with_worker.deleteFSHandler(mount_key)
    else:
        handler_exists = await _fs.idb.has(mount_key)
        if handler_exists:
            handler_exists = await _fs.idb.delete(mount_key)

    if handler_exists:
        interpreter._module.FS.unmount(path)
        if path in mounted:
            del mounted[path]

    return handler_exists