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

# Volumes

> Upload a tarball once and attach it to one or many Declaw sandboxes at create time using the Go SDK.

```go theme={null}
import "github.com/declaw-ai/declaw-go"
```

A **volume** is a tenant-owned blob (gzip-compressed tar archive) that lives in Declaw's object store. You upload a volume once with `CreateVolume()` and attach it to any number of sandboxes at create time via `WithVolumes()`. On boot, Declaw streams the blob from object storage and materializes its regular-file entries under the attachment's `MountPath` before the first command runs.

## `declaw.CreateVolume()`

Upload a tar.gz and register it.

```go theme={null}
data, err := os.ReadFile("dataset.tar.gz")
if err != nil {
    log.Fatal(err)
}
vol, err := declaw.CreateVolume(ctx, "training-set-v1", data)
if err != nil {
    log.Fatal(err)
}
fmt.Println(vol.VolumeID, vol.SizeBytes)
```

<ParamField body="name" type="string" required>
  Human-readable name. The server returns a stable `VolumeID`.
</ParamField>

<ParamField body="data" type="[]byte" required>
  The gzip-compressed tar archive. Must start with gzip magic bytes (`0x1F 0x8B`).
  Pass `nil` or empty for an empty volume.
</ParamField>

**Returns** `(*VolumeInfo, error)`

***

## `declaw.ListVolumes()`

List all volumes owned by the caller.

```go theme={null}
volumes, err := declaw.ListVolumes(ctx)
for _, v := range volumes {
    fmt.Println(v.VolumeID, v.Name, v.SizeBytes)
}
```

**Returns** `([]VolumeInfo, error)`

***

## `declaw.GetVolume()`

Fetch metadata for a single volume.

```go theme={null}
vol, err := declaw.GetVolume(ctx, "vol-abc123")
```

**Returns** `(*VolumeInfo, error)`

***

## `declaw.DownloadVolume()`

Download the contents of a volume as raw bytes.

```go theme={null}
data, err := declaw.DownloadVolume(ctx, "vol-abc123")
if err != nil {
    log.Fatal(err)
}
_ = os.WriteFile("backup.tar.gz", data, 0644)
```

**Returns** `([]byte, error)`

***

## `declaw.DeleteVolume()`

Delete a volume by its ID.

```go theme={null}
err := declaw.DeleteVolume(ctx, "vol-abc123")
```

**Returns** `error`

***

## Attaching to a sandbox

Pass `WithVolumes()` to `Create()`:

```go theme={null}
vol, _ := declaw.CreateVolume(ctx, "dataset", tarGzBytes)

sbx, err := declaw.Create(ctx,
    declaw.WithTemplate("python"),
    declaw.WithTimeout(600),
    declaw.WithVolumes([]declaw.VolumeAttachment{
        {VolumeID: vol.VolumeID, MountPath: "/data"},
    }),
)

// Files are already visible when the first command runs
result, _ := sbx.Commands.Run(ctx, "ls -la /data")
fmt.Println(result.Stdout)
```

The same `VolumeID` can appear in many sandbox-create calls in parallel; each sandbox gets its own materialized copy.

## File-granular volumes (live mounts)

The volumes above are **copy-mode**: a tar.gz hydrated into the sandbox at boot, with writes private to each sandbox. A **file-granular** volume is different — you can edit its files directly from the SDK (no sandbox), and **live-mount** it into a sandbox so reads *and* writes go straight to the shared volume. File-granular volumes have a flat **64 GiB** capacity cap.

### Create a file-granular volume

```go theme={null}
vol, _ := declaw.CreateEmptyVolume(ctx, "scratch")        // empty
vol, _ = declaw.IngestVolume(ctx, "seed", tarGzBytes)     // or pre-populated from a tar.gz
fmt.Println(vol.Backend)   // "juicefs" / "local" (not "tarball")
```

### Edit files without a sandbox — `VolumeFilesFor()`

```go theme={null}
files := declaw.VolumeFilesFor(vol.VolumeID)
files.Write(ctx, "/config/app.json", []byte(`{"k":"v"}`))   // parent dirs auto-created
files.Mkdir(ctx, "/data")
data, _ := files.Read(ctx, "/config/app.json")
entries, _ := files.List(ctx, "/")
for _, e := range entries {
    fmt.Println(e.Path, e.IsDir, e.Size)
}
files.Rename(ctx, "/config/app.json", "/config/app.prod.json")
files.Remove(ctx, "/data", true /* recursive */)
```

`files.Info(ctx, path)` returns a `Version` token; pass it via a `WriteFileOption` for an optimistic compare-and-set write (a 409 means the file changed underneath you).

### Live-mount into a sandbox

```go theme={null}
files := declaw.VolumeFilesFor(vol.VolumeID)
files.Write(ctx, "/greeting.txt", []byte("hello from the files API"))

sbx, _ := declaw.Create(ctx,
    declaw.WithTemplate("base"),
    declaw.WithVolumes([]declaw.VolumeAttachment{
        {VolumeID: vol.VolumeID, MountPath: "/data", Mode: declaw.VolumeModeMount},
    }),
)
// The sandbox reads the files-API write over a live NFS mount...
result, _ := sbx.Commands.Run(ctx, "cat /data/greeting.txt")
fmt.Println(result.Stdout)
// ...and its writes are visible back through the files API immediately:
sbx.Commands.Run(ctx, "echo 'from the sandbox' > /data/out.txt")
out, _ := files.Read(ctx, "/out.txt")
fmt.Println(string(out))
```

Use `declaw.VolumeModeMountRO` for a read-only mount — guest writes are rejected with a read-only-filesystem error. Live mounts require a file-granular volume; copy-mode volumes can only use `declaw.VolumeModeCopy`.

### Mount a sub-path

Mount just part of a volume with `Subpath` (live-mount only — the server rejects it on a copy attach):

```go theme={null}
sbx, _ := declaw.Create(ctx,
    declaw.WithTemplate("base"),
    declaw.WithVolumes([]declaw.VolumeAttachment{
        {VolumeID: vol.VolumeID, MountPath: "/data", Mode: declaw.VolumeModeMount, Subpath: "datasets/train"},
    }),
)
```

## Snapshot a sandbox's files into a volume

Capture filesystem state from a running sandbox into a **new** volume — the source is never modified:

```go theme={null}
// Any absolute in-sandbox path -> new volume
snap, _ := declaw.SnapshotVolume(ctx, sbx.ID, "/workspace/out", "run-42")

// An already-attached volume's mount path -> new volume
checkpoint, _ := declaw.CommitVolume(ctx, sbx.ID, src.VolumeID, "checkpoint")
```

`SnapshotVolume` captures any in-sandbox path; `CommitVolume` captures the mount path of an already-attached volume. Both return a new `*VolumeInfo`; pass `""` for `name` to let the server default it. Synthetic paths (`/proc`, `/sys`, `/dev`) are rejected.

## Advisory locks

Coordinate writers to a shared (live-mounted) volume with advisory **leases** over a `(volume, path)` pair. `Acquire` returns a token you must present to `Renew` / `Release`:

```go theme={null}
locks := declaw.VolumeLocksFor(vol.VolumeID)

lease, _ := locks.Acquire(ctx, "/data/model.bin", 60) // 409 (ConflictError) if already held
locks.Renew(ctx, "/data/model.bin", lease.Token, 60)
status, _ := locks.Status(ctx, "/data/model.bin")      // {Held: true, ExpiresInMs: ...}
locks.Release(ctx, "/data/model.bin", lease.Token)     // (released bool, error)
```

Locks are **advisory** — they coordinate cooperating writers; they don't block I/O from code that ignores them.

***

## Data models

### `VolumeInfo`

```go theme={null}
type VolumeInfo struct {
    VolumeID    string
    OwnerID     string
    Name        string
    BlobKey     string
    SizeBytes   int64
    ContentType string
    CreatedAt   string
    Metadata    map[string]string
    Backend     string // "tarball" (copy) | "local" | "juicefs" (file-granular)
    QuotaBytes  int64  // file-granular volumes; 0 = unlimited
}
```

### `VolumeAttachment`

```go theme={null}
type VolumeAttachment struct {
    VolumeID  string
    MountPath string
    Mode      string // "copy" (default) | "mount" | "mount-ro"; see VolumeMode* constants
    Subpath   string // live-mount only; relative path within the volume
}
```
