Skip to main content
The filesystem API uses the same channel as command execution. No SSH, no network involved. For bulk operations or ongoing file access, volumes are significantly faster since they give the guest direct filesystem access. The filesystem API is better suited for ad-hoc work: pushing generated code into a sandbox, pulling back results, checking logs.

Write a file

sb.fs().write("/app/config.json", r#"{"debug": true}"#).await?;

Read a file

let content = sb.fs().read_to_string("/app/config.json").await?;

List a directory

let entries = sb.fs().list("/app").await?;
for entry in entries {
    println!("{}: {:?}", entry.path, entry.kind);
}

Stream large files

For files too large to fit in memory, stream them in chunks (~3 MiB each).
use futures::StreamExt;

let mut stream = sb.fs().read_stream("/app/data.bin").await?;
while let Some(chunk) = stream.next().await {
    process(chunk?);
}

Copy from host

Copy a file from the host into the sandbox in a single call.
sb.fs().copy_from_host("./local-file.txt", "/app/remote-file.txt").await?;

Extensible backends

The default filesystem backends handle most use cases. For advanced scenarios, you can attach hooks to intercept operations on a volume, or implement a full custom backend.

Hooks

Intercept file reads and writes on a volume without replacing the entire backend. Hooks receive the path and data, and return transformed data. The underlying filesystem handles everything else (permissions, directories, metadata).
use microsandbox::Sandbox;

let key = get_encryption_key();
let sb = Sandbox::builder("encrypted")
    .image("python:3.12")
    .volume("/secrets", |v| v
        .bind("/data/secrets")
        .on_read(move |_path, data| decrypt(data, &key))
        .on_write(move |_path, data| encrypt(data, &key))
    )
    .create().await?;

Custom backend

For full control, implement the FsBackend trait (Rust), class (TypeScript), or protocol (Python). This gives you access to every POSIX operation: read, write, lookup, getattr, readdir, etc. You can delegate to a built-in backend (like PassthroughFs) for operations you don’t need to customize.
use microsandbox::{FsBackend, PassthroughFs};
use std::io;

struct EncryptedFs { key: [u8; 32], inner: PassthroughFs }

impl FsBackend for EncryptedFs {
    fn read(&self, ctx: Context, inode: u64, handle: u64,
            buf: &mut [u8], offset: u64) -> io::Result<usize> {
        let n = self.inner.read(ctx, inode, handle, buf, offset)?;
        self.decrypt_in_place(&mut buf[..n]);
        Ok(n)
    }
    fn write(&self, ctx: Context, inode: u64, handle: u64,
             buf: &[u8], offset: u64) -> io::Result<usize> {
        let encrypted = self.encrypt(buf);
        self.inner.write(ctx, inode, handle, &encrypted, offset)
    }
    // ... remaining methods delegate to self.inner
}

let sb = Sandbox::builder("custom")
    .volume("/secrets", |v| v.backend(EncryptedFs::new(key, "/data")?))
    .create().await?;