> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sailresearch.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Filesystem

> Read, write, and stream files in a running Sailbox

Each Sailbox has a writable state disk. Runtime filesystem APIs operate on that
writable disk; checkpoints preserve it across pause, sleep, resume, cloned
children, and recovery.

The examples below assume a running Sailbox `sb`.

## Write files

Upload bytes or strings into the Sailbox filesystem. Paths must be absolute.
Missing parent directories are created by default.

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write("/workspace/input.txt", "hello\n")
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.write("/workspace/input.txt", "hello\n");
  ```

  ```rust Rust theme={null}
  use sail::WriteOptions;

  sb.fs().write(
      "/workspace/input.txt",
      b"hello\n",
      WriteOptions {
          create_parents: true,
          mode: 0o644,
          ..Default::default()
      },
  )
  .await?;
  ```
</CodeGroup>

Write several complete files in one call when startup code has many small
inputs. `write` writes one file and can stream a file-like source in Python;
`write_files` holds every file's contents in memory.

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write_files({
      "/workspace/a.txt": "first\n",
      "/workspace/b.txt": b"second\n",
  })
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.writeFiles({
    "/workspace/a.txt": "first\n",
    "/workspace/b.txt": Buffer.from("second\n"),
  });
  ```

  ```rust Rust theme={null}
  use sail::WriteOptions;

  sb.fs()
      .write_files(
          [
              ("/workspace/a.txt", &b"first\n"[..]),
              ("/workspace/b.txt", &b"second\n"[..]),
          ],
          WriteOptions::default(),
      )
      .await?;
  ```
</CodeGroup>

Each file is its own request, up to eight at a time, and every file gets the
same options. A batch is not atomic across paths: the first failure stops the
batch, files that already completed stay written, writes already in flight
finish on their own, and the error names the file that failed. A path may
appear only once. Contents are held in memory; use the streaming API for a
large source.

Pass a mode to set POSIX permission bits. When omitted, writes default to
`0o644`.

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write("/workspace/private.txt", "secret\n", mode=0o600)
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.write("/workspace/private.txt", "secret\n", { mode: 0o600 });
  ```

  ```rust Rust theme={null}
  use sail::WriteOptions;

  sb.fs().write(
      "/workspace/private.txt",
      b"secret\n",
      WriteOptions {
          create_parents: true,
          mode: 0o600,
          ..Default::default()
      },
  )
  .await?;
  ```
</CodeGroup>

Disable parent creation if you want writes to fail when parent directories are
missing:

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write("/workspace/data/input.txt", "hello\n", create_parents=False)
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.write("/workspace/data/input.txt", "hello\n", {
    createParents: false,
  });
  ```

  ```rust Rust theme={null}
  use sail::WriteOptions;

  sb.fs().write(
      "/workspace/data/input.txt",
      b"hello\n",
      WriteOptions {
          create_parents: false,
          mode: 0o644,
          ..Default::default()
      },
  )
  .await?;
  ```
</CodeGroup>

## Read files

Fetch a regular file back as bytes:

<CodeGroup>
  ```python Python theme={null}
  data = sb.fs.read("/workspace/input.txt")
  print(data.decode())
  ```

  ```typescript TypeScript theme={null}
  const data = await sb.fs.read("/workspace/input.txt");
  console.log(data.toString());
  ```

  ```rust Rust theme={null}
  let data = sb.fs().read("/workspace/input.txt").await?;
  println!("{}", String::from_utf8_lossy(&data));
  ```
</CodeGroup>

Whole-file reads buffer the full file in memory. For larger files, stream
chunks instead:

<CodeGroup>
  ```python Python theme={null}
  with open("output.bin", "wb") as out:
      for chunk in sb.fs.read_stream("/workspace/output.bin"):
          out.write(chunk)
  ```

  ```typescript TypeScript theme={null}
  import { createWriteStream } from "node:fs";
  import { pipeline } from "node:stream/promises";

  await pipeline(
    (await sb.fs.readStream("/workspace/output.bin")).toReadable(),
    createWriteStream("output.bin"),
  );
  ```

  ```rust Rust theme={null}
  use std::io::Write;

  let mut out = std::fs::File::create("output.bin")?;
  let reader = sb.fs().read_stream("/workspace/output.bin").await?;
  while let Some(chunk) = reader.next().await {
      out.write_all(&chunk?)?;
  }
  ```
</CodeGroup>

## Work with directories

The `fs` namespace also covers directory work. `mkdir` creates a directory and
any missing parents, `ls` lists a directory's immediate entries as structured
records (name, type, size, modified time, mode), `exists` checks a path, and
`remove` deletes a file or directory tree:

<CodeGroup>
  ```python Python theme={null}
  sb.fs.mkdir("/workspace/results")
  sb.fs.write("/workspace/results/input.txt", "hello\n")
  for entry in sb.fs.ls("/workspace/results"):
      print(entry.name, entry.type, entry.size)
  if sb.fs.exists("/workspace/results/input.txt"):
      sb.fs.remove("/workspace/results/input.txt")
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.mkdir("/workspace/results");
  await sb.fs.write("/workspace/results/input.txt", "hello\n");
  for (const entry of await sb.fs.ls("/workspace/results")) {
    console.log(entry.name, entry.type, entry.size);
  }
  if (await sb.fs.exists("/workspace/results/input.txt")) {
    await sb.fs.remove("/workspace/results/input.txt");
  }
  ```

  ```rust Rust theme={null}
  sb.fs().mkdir("/workspace/results").await?;
  sb.fs()
      .write("/workspace/results/input.txt", b"hello\n", Default::default())
      .await?;
  for entry in sb.fs().ls("/workspace/results").await? {
      println!("{} {:?} {}", entry.name, entry.entry_type, entry.size);
  }
  if sb.fs().exists("/workspace/results/input.txt").await? {
      sb.fs().remove("/workspace/results/input.txt").await?;
  }
  ```
</CodeGroup>

## Uploading and downloading directories

Every SDK provides directory transfer methods that move whole trees in one
call. They ship one compressed archive instead of one call per file, so a
tree of many small files transfers quickly:

<CodeGroup>
  ```python Python theme={null}
  # Upload a local directory's contents
  sb.fs.upload_dir("./local_data", "/workspace/data")

  # Download a Sailbox directory to local filesystem
  sb.fs.download_dir("/workspace/results", "./local_results")
  ```

  ```typescript TypeScript theme={null}
  // Upload a local directory's contents
  await sb.fs.uploadDir({
    localDir: "./local_data",
    guestDir: "/workspace/data",
  });

  // Download a Sailbox directory to local filesystem
  await sb.fs.downloadDir({
    guestDir: "/workspace/results",
    localDir: "./local_results",
  });
  ```

  ```rust Rust theme={null}
  use std::path::Path;

  // Upload a local directory's contents
  sb.fs().upload_dir(Path::new("./local_data"), "/workspace/data").await?;

  // Download a Sailbox directory to local filesystem
  sb.fs().download_dir("/workspace/results", Path::new("./local_results")).await?;
  ```
</CodeGroup>

Entries the transfer does not name are left in place, and a same-named file
is replaced; uploaded files preserve their permission bits (the setuid,
setgid, and sticky bits are cleared). Only download directories of ordinary
files: system trees like `/proc` or `/sys` hold files that cannot be read
as plain data, and downloading them fails. A file that is being written
while the download runs is captured as it is at that moment, the way
copying a live file would; download after writers finish for a consistent
copy. On Windows, a directory that contains symbolic links cannot be
downloaded, since Windows restricts creating them. The Sailbox image must
provide `tar` and `gzip`, which the default images do.

## Persist state with checkpoints

Runtime writes live on the Sailbox state disk. Checkpoint after important
writes if you want recovery and future resumes to start from that point:

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write("/workspace/config.json", '{"ready": true}\n')
  sb.checkpoint()
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.write("/workspace/config.json", '{"ready": true}\n');
  await sb.checkpoint();
  ```

  ```rust Rust theme={null}
  use sail::CheckpointOptions;

  use sail::WriteOptions;

  sb.fs().write(
      "/workspace/config.json",
      b"{\"ready\": true}\n",
      WriteOptions {
          create_parents: true,
          mode: 0o644,
          ..Default::default()
      },
  )
  .await?;
  sb.checkpoint(CheckpointOptions::default()).await?;
  ```
</CodeGroup>

See [Lifecycle](/sailboxes-lifecycle) for checkpoint, start-from-checkpoint,
pause, sleep, and resume behavior.

## Runtime files vs image files

Use runtime filesystem APIs for inputs, outputs, logs, generated artifacts, and
data that changes per Sailbox. Use [Images](/sailboxes-images) for packages,
source files, and static assets that should be present before the VM boots.
