# div storage

> Lists a volume, puts files on it and gets them back — checkpoints streamed to disk — one file per request, so a file that fails leaves the others whole.

```bash
div storage ls contracts-v3
div storage put contracts-v3 ./data
div storage put contracts-v3 ./data.csv raw/data.csv
div storage get training-volume-1 log/training.jsonl
div storage get training-volume-1 log/training.jsonl - | tail -5
```

A volume is where the hardware can reach your data. `div storage` moves files between it and this machine; a run mounts it at `/mnt/<volume>`. Every volume that exists when a run starts is mounted, and a run cannot make one partway through: see [the runtime](https://docs.divergentlabs.xyz/compute/runtime.md#filesystem).

## Usage

```text
div storage ls <volume> [directory]
div storage put <volume> <path> [destination]
div storage get <volume> <path> [destination]
```

## ls

`ls` lists one directory of a volume, the root unless one is named. It never walks the tree: a volume with a hundred thousand checkpoints on it is read a directory at a time.

```text
$ div storage ls mask-data openpii
                    openpii/shards/
229 MB  2026-09-23  openpii/train.parquet
 21 MB  2026-09-23  openpii/validation.parquet
```

Directories come first and end in a slash. The path is the last column, so `awk '{print $NF}'` is a list of paths, and `--json` prints the entries as the API sends them. The listing is read from the volume when it is asked for, so it is what a run would mount.

## put

`put` sends a file or a directory to a volume. A volume `put` names is created if it is not there yet.

A directory is walked and sent file by file, with the file as the body of each request, so what lands is whole files rather than half of an archive.

A file that fails is named as it fails, and the rest are still sent. `put` then says how many landed, prints the command that sends each missing file again, and exits 1:

```text
openpii/train.parquet  the supplier could not be reached
put 6 of 7 files, 771 MB on mask-data
send it again with  div storage put mask-data openpii/train.parquet openpii/train.parquet
error 1 file did not land on mask-data
```

Sending a file again is safe: a put replaces whatever is at its path. A refusal every later file would get too — the key, the credit, the org's storage — stops the transfer at once and names the file it stopped at.

A directory lands under its own name — `./data` becomes `data/…` on the volume, and nothing is silently flattened into the root. A destination puts it somewhere else:

```bash
div storage put contracts-v3 ./data            # → data/…
div storage put contracts-v3 ./data corpus     # → corpus/…
div storage put contracts-v3 ./data.csv raw/data.csv
```

On a terminal it shows progress as it goes, and ends with a count:

```text
put 212 files, 1.4 GB on contracts-v3
```

The walk follows the same rules as [`div run`](https://docs.divergentlabs.xyz/compute/cli/run.md#the-working-directory-is-shipped): inside a git repository it sends what `git ls-files` would list, and outside one it skips `node_modules`, `__pycache__`, `.venv`, build output and anything beginning with `.env`.

## get

`get` reads a file back. It lands under its own name in the working directory, or at the destination when one is given:

```bash
div storage get training-volume-1 log/training.jsonl              # → ./training.jsonl
div storage get training-volume-1 log/training.jsonl logs/t.jsonl # → ./logs/t.jsonl
```

The file is written as it arrives, never held whole, so a 5 GB checkpoint needs 5 GB of disk and no more memory than a small file. On a terminal, stderr shows how far along it is — `final/model.safetensors  1.2 GB of 5.1 GB  6.1 MB/s`. It goes to `<destination>.part` first and takes its real name only once every byte has arrived. A download that stops short of its length is an error, the `.part` is removed, and nothing under the real name is ever a partial file.

A directory is walked and fetched file by file, into a directory of its own name unless a destination is given:

```bash
div storage get rubric-runs sft-v0c/final                     # → ./final/…
div storage get rubric-runs sft-v0c/final checkpoints/sft-v0c # → ./checkpoints/sft-v0c/…
```

As with `put`, a file that fails does not stop the others; the ones that did not arrive are named at the end, each with the command that gets it again.

`-` as the destination writes the file to stdout, so it can be piped into something that reads. A reader that has seen enough closing the pipe — `| head` — is not a failure. A directory cannot go to stdout.

When the file is not there, `get` says which half is missing: `training-volume-1 has no log/training.jsonl`, or `there is no volume called training-volume-1`.

## Large files

Reading back has no size limit: `get` streams from the volume to your disk, and a checkpoint takes as long as your link needs for it. Writing is capped. Files over 256 MB do not cross the API on the way up. Below that, the control plane has five minutes to hand each file on to the hardware, so the real ceiling is what that link moves in five minutes: about 190 MB at 5 Mbit/s. A file that big should be written to the volume from inside a run, which writes it where it already is — download the dataset in the run rather than uploading it to one.

The dashboard's file explorer opens the same volume at `/storage/<name>`, read live, so what is on screen is what a run would mount.
