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

# Splitter

> Cut large backup archives into fixed-size parts before upload, and reassemble them with cat when needed.

`split` cuts the final archive into fixed-size parts before upload. snapr applies it **after** compression and (if configured) encryption, so each part is a slice of the encrypted, compressed bytes — not a self-contained mini-archive.

Use it when:

* a storage backend rejects objects above some size (FTP/SFTP server quotas, email attachment limits, providers without S3 multipart upload),
* you want backups to fit on removable media of a known capacity,
* a flaky network makes re-uploading one failed part cheaper than re-uploading the whole archive.

## Fields

| Option      | Type   | Required | Default | Description                                                                                                                                                                                                       |
| ----------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chunkSize` | string | Yes      | —       | Size per part. Accepts `B`, `K`/`KB`/`KiB`, `M`/`MB`/`MiB`, `G`/`GB`/`GiB`, `T`/`TB`/`TiB`, or a bare number of bytes. Decimal and binary suffixes both mean powers of 1024, so `1MB` = `1MiB` = 1,048,576 bytes. |

## Example

```yaml snapr.yaml theme={null}
jobs:
  - name: huge-postgres
    schedule: '0 3 * * *'
    compression: tar.gz
    sources:
      - type: postgresql
        host: db.internal
        username: postgres
        password: env:PG_PASSWORD
        database: app
    storages:
      - name: offsite
        type: sftp
        host: backup.example.com
        username: backup
        privateKey: /etc/snapr/keys/sftp
        path: /snapr
    retention:
      last: 14
    encryption:
      type: openssl
      password: env:BACKUP_ENC_PASSWORD
    split:
      chunkSize: 1GB
```

## How parts are named

Each part gets the original archive name plus a `.part-` suffix with three lowercase letters, counting in base 26: `aaa`, `aab`, … `zzz`. That caps an archive at **17,576 parts**. With a 1 GiB chunk that is a 17.1 TiB ceiling — raise `chunkSize` if you ever get close.

## On-disk layout

A split snapshot lives in a wrapper directory inside the [per-job folder](/configuration/storages/overview#on-disk-layout). Parts are plain files inside it:

```
<storage.path>/huge-postgres/
  huge-postgres-20260507-030000.tar.gz.enc.parts-3-3221225472/
    huge-postgres-20260507-030000.tar.gz.enc.part-aaa
    huge-postgres-20260507-030000.tar.gz.enc.part-aab
    huge-postgres-20260507-030000.tar.gz.enc.part-aac
```

The wrapper name encodes the **part count** and the **total size in bytes** (`.parts-3-3221225472` means 3 parts, about 3 GiB). snapr reads both from the directory name alone, so listing a job's backups never descends into the wrapper or stats individual parts — it stays one storage call regardless of part count.

The wrapper is created at upload time and never modified afterward. Retention keeps the whole wrapper or deletes it entirely.

<Note>
  If the archive fits in a single chunk, snapr skips the wrapper and uploads the single part (still named `.part-aaa`)
  directly into the job folder. A wrapper with one part is never produced.
</Note>

## Retention treats a set as one backup

One wrapper directory counts as one logical backup. `retention.last: 14` keeps the **14 newest snapshots**, no matter how many parts each produced. Rotation removes the wrapper and all its parts in one operation.

## Downloading a split backup

The web UI and `GET /api/v1/jobs/{name}/backups/{filename}/download` accept the **set ID** — the original archive name without the wrapper suffix and without `.part-XXX`. snapr opens each part on the storage, streams them back-to-back, and serves the result as one continuous download. You get a single file, no manual concatenation.

The UI also offers per-part download, useful when one full-archive transfer is impractical.

<Note>
  For bunny.net Storage behind a signed Pull Zone, full-set download is not supported — one HTTP redirect cannot
  represent N parts. "Download all" returns `501 Not Implemented` there. Per-part download still works, since each part
  gets its own redirect to a signed URL. For full-set streaming, use a non-redirect storage (Local, S3, SFTP, WebDAV) as
  the download source.
</Note>

## Reassembling parts manually

If you copy a wrapper directory off the storage yourself, concatenate the parts in lexicographic order.

<Steps>
  <Step title="Concatenate the parts">
    The `aaa`, `aab`, `aac` suffixes sort lexicographically in write order, and shell globs expand in the same order, so a plain `cat` is safe:

    ```bash theme={null}
    cd huge-postgres-20260507-030000.tar.gz.enc.parts-3-3221225472
    cat *.part-* > ../archive.tar.gz.enc
    cd ..
    ```
  </Step>

  <Step title="Decrypt (if the job uses encryption)">
    ```bash theme={null}
    openssl enc -d -aes-256-cbc -pbkdf2 \
      -in archive.tar.gz.enc \
      -out archive.tar.gz \
      -pass env:BACKUP_ENC_PASSWORD
    ```

    See [Encryption](/configuration/encryption) for details.
  </Step>

  <Step title="Extract">
    ```bash theme={null}
    tar -xzf archive.tar.gz
    ```
  </Step>
</Steps>

## Toggling split on or off

Adding or removing the `split` block between runs is safe. Existing snapshots keep their layout — wrapper directories from past split runs and plain archives from non-split runs coexist in the same job folder. Listing, retention, and download handle both transparently.

## Notes

* `chunkSize` is validated at config load. An unparseable value fails startup with a validation error.
* Splitting runs purely in Go — no external `split` binary required.
* A part smaller than `chunkSize` is always the **last** part. Don't estimate `chunkSize` from an arbitrary part's size.
