# Volume HTTP

Use direct Volume file operations to operate on files in a Volume by volume ID, without mounting the Volume into a Sandbox first.

Start with the Go, Python, TypeScript SDKs, or the `s0` CLI. The endpoint reference below is for advanced integrations and custom clients that need to call the HTTP and WebSocket API directly.

<Callout variant="info">
Requests still go through the normal gateway chain and team-scoped auth. For `s0fs` Volumes, `storage-proxy` lazily attaches the Volume and reclaims idle direct mounts later. For `s3` backend Volumes, direct file requests are supported only while an active ctld mount owner can serve the request. See [Volume Backends](/docs/sandbox/volume/backends).
</Callout>

## Base Model

- Every path is resolved relative to the Volume root.
- Use absolute logical paths such as `/docs/readme.txt`.
- Read operations use `sandboxvolumefile:read`.
- Write, move, mkdir, and delete operations use `sandboxvolumefile:write`.

<Callout variant="info">
The client helpers below call the same `SandboxVolume file` HTTP endpoints documented on this page. They do not require mounting an `s0fs` Volume into a Sandbox first.
</Callout>

## SDK and CLI Quick Start

Use these helpers for normal direct Volume file workflows.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `ctx := context.Background()
volumeID := "vol_abc123"

_, err := client.MkdirVolumeFile(ctx, volumeID, "/docs", true)
if err != nil {
    log.Fatal(err)
}

_, err = client.WriteVolumeFile(ctx, volumeID, "/docs/readme.txt", []byte("Hello from Volume HTTP"))
if err != nil {
    log.Fatal(err)
}

summary, err := client.UploadVolumeDirectory(ctx, volumeID, "./dist", "/apps/current")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("uploaded files=%d bytes=%d\\n", summary.Files, summary.Bytes)

data, err := client.ReadVolumeFile(ctx, volumeID, "/docs/readme.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

info, err := client.StatVolumeFile(ctx, volumeID, "/docs/readme.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("size=%d mode=%s\\n", info.Size, info.Mode)

entries, err := client.ListVolumeFiles(ctx, volumeID, "/docs")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("entries=%d\\n", len(entries))

_, err = client.MoveVolumeFile(ctx, volumeID, "/docs/readme.txt", "/docs/readme-old.txt")
if err != nil {
    log.Fatal(err)
}

_, err = client.DeleteVolumeFile(ctx, volumeID, "/docs/readme-old.txt")
if err != nil {
    log.Fatal(err)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `volume_id = "vol_abc123"

client.volumes.mkdir(volume_id, "/docs", recursive=True)
client.volumes.write_file(volume_id, "/docs/readme.txt", b"Hello from Volume HTTP")

summary = client.volumes.upload_directory(volume_id, "./dist", "/apps/current")
print(f"uploaded files={summary.files} bytes={summary.bytes_}")

data = client.volumes.read_file(volume_id, "/docs/readme.txt")
print(data.decode())

info = client.volumes.stat_file(volume_id, "/docs/readme.txt")
print(f"size={info.size} mode={info.mode}")

entries = client.volumes.list_files(volume_id, "/docs")
print(f"entries={len(entries)}")

client.volumes.move_file(volume_id, "/docs/readme.txt", "/docs/readme-old.txt")
client.volumes.delete_file(volume_id, "/docs/readme-old.txt")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `import { readFile } from "node:fs/promises";

const volumeId = "vol_abc123";

await client.volumes.mkdir(volumeId, "/docs", true);
await client.volumes.writeFile(volumeId, "/docs/readme.txt", "Hello from Volume HTTP");

const archive = await readFile("dist.tar");
const summary = await client.volumes.importArchive(volumeId, "/apps/current", archive);
console.log("uploaded files=", summary.files, "bytes=", summary.bytes);

const data = await client.volumes.readFile(volumeId, "/docs/readme.txt");
console.log(Buffer.from(data).toString("utf8"));

const info = await client.volumes.statFile(volumeId, "/docs/readme.txt");
console.log("size=", info.size, "mode=", info.mode);

const entries = await client.volumes.listFiles(volumeId, "/docs");
console.log("entries=", entries.length);

await client.volumes.moveFile(volumeId, "/docs/readme.txt", "/docs/readme-old.txt");
await client.volumes.deleteFile(volumeId, "/docs/readme-old.txt");`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 volume files mkdir vol_abc123 /docs --parents
s0 volume files write vol_abc123 /docs/readme.txt --data "Hello from Volume HTTP"
s0 volume files upload vol_abc123 ./dist /apps/current --recursive
s0 volume files cat vol_abc123 /docs/readme.txt
s0 volume files stat vol_abc123 /docs/readme.txt
s0 volume files ls vol_abc123 /docs
s0 volume files mv vol_abc123 /docs/readme.txt /docs/readme-old.txt
s0 volume files rm vol_abc123 /docs/readme-old.txt`
    }
  ]}
/>

### Client Method Mapping

| Operation | Go | Python | TypeScript | CLI |
|-----------|----|--------|------------|-----|
| Read file | `ReadVolumeFile` | `client.volumes.read_file` | `client.volumes.readFile` | `s0 volume files cat` |
| Stat path | `StatVolumeFile` | `client.volumes.stat_file` | `client.volumes.statFile` | `s0 volume files stat` |
| List directory | `ListVolumeFiles` | `client.volumes.list_files` | `client.volumes.listFiles` | `s0 volume files ls` |
| Write file | `WriteVolumeFile` | `client.volumes.write_file` | `client.volumes.writeFile` | `s0 volume files write` |
| Create directory | `MkdirVolumeFile` | `client.volumes.mkdir` | `client.volumes.mkdir` | `s0 volume files mkdir` |
| Move or rename | `MoveVolumeFile` | `client.volumes.move_file` | `client.volumes.moveFile` | `s0 volume files mv` |
| Delete path | `DeleteVolumeFile` | `client.volumes.delete_file` | `client.volumes.deleteFile` | `s0 volume files rm` |
| Upload local file | manual local read + `WriteVolumeFile` | `client.volumes.write_file` | `client.volumes.writeFile` | `s0 volume files upload` |
| Upload directory/archive | `UploadVolumeDirectory` / `ImportVolumeArchive` | `client.volumes.upload_directory` / `client.volumes.import_archive` | `client.volumes.importArchive` | `s0 volume files upload --recursive` |
| Download to local file | manual local write + `ReadVolumeFile` | `client.volumes.read_file` | `client.volumes.readFile` | `s0 volume files download` |
| Watch path | `WatchVolumeFiles` | `client.volumes.watch_files` | `client.volumes.watchFiles` | `s0 volume files watch` |

### Directory Upload

Use directory upload for build artifacts, framework bundles, and other multi-file trees.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `summary, err := client.UploadVolumeDirectory(ctx, volumeID, "./dist", "/apps/current")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("uploaded files=%d dirs=%d symlinks=%d bytes=%d\\n",
    summary.Files,
    summary.Directories,
    summary.Symlinks,
    summary.Bytes,
)`
    },
    {
      label: "Python",
      language: "python",
      code: `summary = client.volumes.upload_directory(volume_id, "./dist", "/apps/current")

print(
    f"uploaded files={summary.files} "
    f"dirs={summary.directories} "
    f"symlinks={summary.symlinks} "
    f"bytes={summary.bytes_}"
)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `import { readFile } from "node:fs/promises";

const archive = await readFile("dist.tar");
const summary = await client.volumes.importArchive(volumeId, "/apps/current", archive);

console.log("uploaded files=", summary.files, "bytes=", summary.bytes);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 volume files upload vol_abc123 ./dist /apps/current --recursive`
    }
  ]}
/>

### FileInfo

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | File or directory name |
| `path` | string | Logical path inside the Volume |
| `type` | string | `file`, `dir`, or `symlink` |
| `size` | integer | Size in bytes |
| `mode` | string | Unix mode bits such as `0644` |
| `mod_time` | string | Last modification time |
| `is_link` | boolean | Whether the entry is a symlink |
| `link_target` | string | Symlink target when available |

## Write File

Write binary content to a file. Creates parent directories automatically when needed.

<Endpoint method="POST">
/api/v1/sandboxvolumes/{'{id}'}/files?path=/docs/readme.txt
</Endpoint>

### Request Body

Raw bytes. For SDK and CLI usage, see the quick start and method mapping above.

---

## Read File

Read a file from a Volume.

<Endpoint method="GET">
/api/v1/sandboxvolumes/{'{id}'}/files?path=/docs/readme.txt
</Endpoint>

By default the response body is raw bytes.

If you set `Accept: application/json`, the API returns a base64 JSON payload instead:

Example response:

```json
{
    "success": true,
    "data": {
        "content": "SGVsbG8sIFZvbHVtZSEK",
        "encoding": "base64"
    }
}
```

---

## Create Directory

Create a directory with the same endpoint used for file writes.

<Endpoint method="POST">
/api/v1/sandboxvolumes/{'{id}'}/files?path=/project/src&mkdir=true&recursive=true
</Endpoint>

For SDK and CLI usage, see the create-directory row in the method mapping above.

---

## Stat Path

Return metadata for a file or directory.

<Endpoint method="GET">
/api/v1/sandboxvolumes/{'{id}'}/files/stat?path=/docs/readme.txt
</Endpoint>

Example response:

```json
{
    "success": true,
    "data": {
        "name": "readme.txt",
        "path": "/docs/readme.txt",
        "type": "file",
        "size": 15,
        "mode": "0644",
        "mod_time": "2026-04-03T10:00:00Z",
        "is_link": false
    }
}
```

---

## List Directory

List the direct children of a directory.

<Endpoint method="GET">
/api/v1/sandboxvolumes/{'{id}'}/files/list?path=/docs
</Endpoint>

Example response:

```json
{
    "success": true,
    "data": {
        "entries": [
            {
                "name": "readme.txt",
                "path": "/docs/readme.txt",
                "type": "file",
                "size": 15,
                "mode": "0644",
                "mod_time": "2026-04-03T10:00:00Z",
                "is_link": false
            },
            {
                "name": "images",
                "path": "/docs/images",
                "type": "dir",
                "size": 0,
                "mode": "0755",
                "mod_time": "2026-04-03T10:00:00Z",
                "is_link": false
            }
        ]
    }
}
```

---

## Move or Rename

Move or rename a file or directory.

<Endpoint method="POST">
/api/v1/sandboxvolumes/{'{id}'}/files/move
</Endpoint>

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `source` | string | Source logical path |
| `destination` | string | Destination logical path |

For SDK and CLI usage, see the move-or-rename row in the method mapping above.

---

## Delete File or Directory

Delete a file or recursively delete a directory.

<Endpoint method="DELETE">
/api/v1/sandboxvolumes/{'{id}'}/files?path=/docs/readme.txt
</Endpoint>

For SDK and CLI usage, see the delete-path row in the method mapping above.

---

## Watch for Changes

Watch a path for file events over WebSocket.

<Endpoint method="GET">
/api/v1/sandboxvolumes/{'{id}'}/files/watch
</Endpoint>

### WebSocket Protocol

After connecting, send JSON messages to subscribe and unsubscribe.

Client messages:

| Action | Description | Example |
|--------|-------------|---------|
| `subscribe` | Start watching a path prefix | `{"action":"subscribe","path":"/docs","recursive":true}` |
| `unsubscribe` | Stop a previous watch | `{"action":"unsubscribe","watch_id":"watch-123"}` |

Server messages:

| Type | Description | Example |
|------|-------------|---------|
| `subscribed` | Subscription accepted | `{"type":"subscribed","watch_id":"watch-123","path":"/docs"}` |
| `event` | File change event | `{"type":"event","watch_id":"watch-123","event":"write","path":"/docs/readme.txt"}` |
| `unsubscribed` | Watch removed | `{"type":"unsubscribed","watch_id":"watch-123"}` |
| `error` | Request rejected | `{"type":"error","error":"invalid path"}` |

Event names currently include:

- `create`
- `write`
- `remove`
- `rename`
- `chmod`
- `invalidate`

### Client Watch Examples

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `watchCtx, cancel := context.WithCancel(context.Background())
defer cancel()

events, errs, unsubscribe, err := client.WatchVolumeFiles(watchCtx, volumeID, "/docs", true)
if err != nil {
    log.Fatal(err)
}
defer func() { _ = unsubscribe() }()

for {
    select {
    case ev, ok := <-events:
        if !ok {
            return
        }
        if ev.Type == "event" {
            fmt.Printf("%s %s\\n", ev.Event, ev.Path)
        }
    case err, ok := <-errs:
        if ok && err != nil {
            log.Fatal(err)
        }
        return
    }
}`
    },
    {
      label: "Python",
      language: "python",
      code: `stream = client.volumes.watch_files(volume_id, "/docs", recursive=True)

try:
    for event in stream.iter_events():
        print(f"{event.event} {event.path}")
finally:
    stream.unsubscribe()`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const watcher = await client.volumes.watchFiles(volumeId, "/docs", true);

try {
    for await (const message of watcher.events()) {
        if (message.type === "event") {
            console.log(message.event, message.path);
        }
        if (message.type === "error") {
            throw new Error(message.error ?? "watch failed");
        }
    }
} finally {
    watcher.close();
}`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 volume files watch vol_abc123 /docs --recursive`
    }
  ]}
/>

---

## Notes

- `GET /files`, `GET /stat`, and `GET /list` require a `path` query parameter.
- `POST /files` with `mkdir=true` creates directories instead of writing file content.
- Directory uploads use a tar stream internally and are not atomic. Deployment flows that require rollback should import into a fresh Volume or snapshot before switching traffic.
- `DELETE /files` is recursive for directories.
- File size is currently capped by the direct HTTP handler. Large transfer workflows should prefer snapshots when appropriate.

---

## Next Steps

<CardGroup>
  <Card title="Snapshots" href="/docs/sandbox/volume/snapshots" cta="Continue">
    Create and restore point-in-time volume snapshots.
  </Card>

  <Card title="Fork" href="/docs/sandbox/volume/fork" cta="Continue">
    Fork volumes when workflows need isolated writable branches.
  </Card>
</CardGroup>
