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

# Direct Volume Files

> Manage persistent volume files without starting a machine.

Direct volume paths are absolute volume-internal paths rooted at `/`.
Relative paths are rejected, `..` cannot escape the volume root, and there is
no machine `cwd` or `user` context for direct volume file operations.

## Read and write

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  from nullspace import Volume

  volume = Volume.from_name("team-data", create_if_missing=True)
  volume.files.make_dir("/datasets")
  volume.files.write("/datasets/report.txt", "ready\n")
  print(volume.files.read("/datasets/report.txt"))
  print(volume.files.read("/datasets/report.txt", format="bytes"))
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  printf "ready\n" | nullspace volume write team-data /datasets/report.txt
  nullspace volume read team-data /datasets/report.txt
  nullspace volume read team-data /datasets/report.txt --encoding base64
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/mkdir" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets"}'

  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/write" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets/report.txt", "content": "ready\n"}'

  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/read" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets/report.txt"}'
  ```
</CodeGroup>

## List and inspect

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  for item in volume.files.list("/datasets"):
      print(item.path, item.size)

  print(volume.files.exists("/datasets/report.txt"))
  print(volume.files.info("/datasets/report.txt").size)
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace volume ls-files team-data /datasets
  nullspace volume exists team-data /datasets/report.txt
  nullspace volume info team-data /datasets/report.txt
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  curl -X GET "${NULLSPACE_API_URL}/v1/volumes/vol_123/files?path=/datasets" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}"

  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/exists" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets/report.txt"}'

  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/info" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets/report.txt"}'
  ```
</CodeGroup>

## Search and replace

```python theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
matches = volume.files.find_files("/datasets", "ready")
paths = volume.files.search_files("/datasets", "*.txt")
result = volume.files.replace_in_files(paths, "ready", "done", dry_run=True)
print(result.dry_run, result.replacements, result.files_modified)
```

`VolumeReplaceResult` also includes the matched file list, so dry runs can show
the planned changes before writing.

## Move and remove

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  volume.files.rename("/datasets/report.txt", "/datasets/report-old.txt")
  volume.files.set_permissions("/datasets/report-old.txt", "0644")
  volume.files.remove("/datasets/report-old.txt")
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace volume mv team-data /datasets/report.txt /datasets/report-old.txt
  nullspace volume chmod team-data /datasets/report-old.txt 0644
  nullspace volume rm team-data /datasets/report-old.txt
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/rename" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"old_path": "/datasets/report.txt", "new_path": "/datasets/report-old.txt"}'

  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/permissions" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets/report-old.txt", "permissions": "0644"}'

  curl -X POST "${NULLSPACE_API_URL}/v1/volumes/vol_123/files/remove" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/datasets/report-old.txt"}'
  ```
</CodeGroup>

## Watch

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  def on_event(event):
      print(event.type, event.path)

  with volume.files.watch_dir("/", on_event=on_event, recursive=True):
      volume.files.write("/datasets/changed.txt", "changed\n")
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace volume watch team-data / --recursive --timeout 30
  ```
</CodeGroup>

Volume watch is not a plain REST call: it streams change events over the volume
data plane rather than a single request/response endpoint, so there is no
`curl` equivalent.

Direct volume watches are best-effort notifications from one selected
data-plane host. In multi-host deployments, mutations performed through another
host may require a resync; clients should treat `resync_required` as a prompt
to list or stat the watched tree again.

`volume.files.replace_in_files()` returns a `VolumeReplaceResult`. Machine
filesystem replacement returns a plain integer count.

Direct watches are opened against a concrete volume ID under the hood. If you
look up a volume by name, the SDK resolves it first and then watches that ID.

## Batch writes

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  from nullspace import BatchWriteError

  try:
      volume.files.write_files([
          ("/datasets/a.txt", "a\n"),
          ("/datasets/b.txt", "b\n"),
      ])
  except BatchWriteError as exc:
      print(exc.successes, exc.failures)
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace volume write-files team-data '[["/datasets/a.txt","a\n"],["/datasets/b.txt","b\n"]]'
  ```
</CodeGroup>

There is no dedicated volume batch-write REST route; over HTTP, issue one
`POST /v1/volumes/{id}/files/write` per file.

## Async

The async client mirrors the sync surface above. Volume content search and
replace (shown earlier) are SDK-only — the CLI does not expose them.

```python theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
from nullspace import AsyncVolume

volume = await AsyncVolume.from_name("team-data", create_if_missing=True)
await volume.files.write("/hello.txt", "persistent\n")
print(await volume.files.read("/hello.txt"))
```

## Related

* [Volume transfers](./transfers)
* [Filesystem](../filesystem)
* [WebSocket protocol](../reference/websocket-protocol)
