Back to Microsandbox

Filesystem

docs/sandboxes/filesystem.mdx

0.4.56.3 KB
Original Source

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

<CodeGroup> ```rust Rust sb.fs().write("/app/config.json", r#"{"debug": true}"#).await?; ```
typescript
await sb.fs().write("/app/config.json", '{"debug": true}');
python
await sb.fs.write("/app/config.json", b'{"debug": true}')
</CodeGroup>

Read a file

<CodeGroup> ```rust Rust let content = sb.fs().read_to_string("/app/config.json").await?; ```
typescript
const content = await sb.fs().readToString("/app/config.json");
python
content = await sb.fs.read_text("/app/config.json")
</CodeGroup>

List a directory

<CodeGroup> ```rust Rust let entries = sb.fs().list("/app").await?; for entry in entries { println!("{}: {:?}", entry.path, entry.kind); } ```
typescript
const entries = await sb.fs().list("/app");
for (const entry of entries) {
    console.log(`${entry.path}: ${entry.kind}`);
}
python
entries = await sb.fs.list("/app")
for entry in entries:
    print(f"{entry.path}: {entry.kind}")
</CodeGroup>

Stream large files

For files too large to fit in memory, use streaming. Data is transferred in chunks of approximately 3 MiB each.

<CodeGroup> ```rust Rust let mut stream = sb.fs().read_stream("/app/data.bin").await?; while let Some(chunk) = stream.recv().await? { process(&chunk); } ```
typescript
const stream = await sb.fs().readStream("/app/data.bin");
for await (const chunk of stream) {
    processChunk(chunk); // chunk is a Uint8Array
}
python
stream = await sb.fs.read_stream("/app/data.bin")
async for chunk in stream:
    process(chunk)
</CodeGroup>

Copy from host

Copy a file from the host machine into the sandbox in a single call.

<CodeGroup> ```rust Rust sb.fs().copy_from_host("./local-file.txt", "/app/remote-file.txt").await?; ```
typescript
await sb.fs().copyFromHost("./local-file.txt", "/app/remote-file.txt");
python
await sb.fs.copy_from_host("./local-file.txt", "/app/remote-file.txt")
</CodeGroup> <Tip> If you need to transfer many files at once, consider using a [bind-mounted volume](/sandboxes/volumes) instead. Volumes give the guest direct filesystem access, whereas the filesystem API transfers each file individually. For bulk operations, volumes are significantly faster. </Tip>

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

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).

<CodeGroup> ```rust Rust 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?;


```typescript TypeScript
import { Sandbox } from "microsandbox";

// Hooks are coming soon. The planned shape will sit alongside the regular
// mount methods on the volume builder, e.g.:
//
//   .volume("/secrets", (m) => m
//       .bind("/data/secrets")
//       .onRead((path, data) => decrypt(data, key))
//       .onWrite((path, data) => encrypt(data, key)),
//   )
//
// For now the closest equivalent is a plain bind:
await using sb = await Sandbox.builder("encrypted")
    .image("python")
    .volume("/secrets", (m) => m.bind("/data/secrets"))
    .create();
python
from microsandbox import Sandbox, Volume

key = get_encryption_key()
sb = await Sandbox.create(
    "encrypted",
    image="python",
    volumes={
        "/secrets": Volume.bind("/data/secrets",
            on_read=lambda path, data: decrypt(data, key),
            on_write=lambda path, data: encrypt(data, key),
        ),
    },
)
</CodeGroup>

Custom backend <sup><sup>coming soon</sup></sup>

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.

<CodeGroup> ```rust Rust 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?;


```typescript TypeScript
// FsBackend is coming soon. The planned API will let you plug a custom
// backend into a volume:
//
//   class EncryptedFs implements FsBackend {
//     // ... implement read, write, and other required methods
//   }
//
//   const sb = await Sandbox.builder("custom")
//     .volume("/secrets", (m) => m.backend(new EncryptedFs(key, "/data")))
//     .create();
python
from microsandbox import FsBackend, Sandbox, Volume

# Implement the FsBackend interface
class EncryptedFs(FsBackend):
    # ... implement read, write, and other required methods
    pass

sb = await Sandbox.create(
    "custom",
    volumes={
        "/secrets": Volume.backend(EncryptedFs(key, "/data")),
    },
)
</CodeGroup>