Skip to main content
The filesystem API lets you read, write, and manage files inside a sandbox without mounting volumes or SSH. It uses the same channel as command execution, so it doesn’t touch the sandbox’s network. Handy for pushing generated code into a sandbox, pulling back results, or inspecting logs without having to pre-mount a shared directory.

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, use streaming. Data is transferred in chunks of approximately 3 MiB each.
let mut stream = sb.fs().read_stream("/app/data.bin").await?;
while let Some(chunk) = stream.recv().await? {
    process(&chunk);
}

Copy from host

Copy a file from the host machine into the sandbox in a single call.
sb.fs().copy_from_host("./local-file.txt", "/app/remote-file.txt").await?;
If you need to transfer many files at once, consider using a bind-mounted volume instead. Volumes give the guest direct filesystem access, whereas the filesystem API transfers each file individually. For bulk operations, volumes are significantly faster.

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 coming soon

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")
    .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 coming soon

For full control, implement the FsBackend trait (Rust) or class (TypeScript). 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?;