Restoring without NBackup

NBackup’s artifacts are plain GNU-tar archives, compressed (and optionally encrypted) with stock tools. Recovery never requires NBackup — this page is the by-hand procedure for when the binary, config, or catalog are all gone. For normal restores use nb recover; to rehearse this exact bare-tools path (and have the commands printed for you), run nb drill --tier stock.

The on-disk layout is described in Concepts: each archive is a clean NNNNNN-<dle>-L<n>.tar.<ext> payload with a .hdr sidecar, plus a small commit footer and member index per archive.

A single full

The disk payload is a clean tar.<ext> — no header to skip:

zstd -dc 000000-app01-home-L0.tar.zst | tar -xf -

(gzip -dc for a .tar.gz; a none scheme is a plain .tar.)

A full + its incrementals

Replay one archive per level — the newest of each, from the last full forward — in level order. Each level is cumulative since the one below, so only its newest dump is needed. Replaying an older same-level rerun re-applies GNU tar’s rename/delete directives and aborts (tar: Cannot rename …).

Order runs by date then same-day sequence. A plain glob mis-sorts a .2 rerun before its own date (since '.' < '/'), so normalize first:

dle=app01-home
runs=$(ls -d runs/run-* | sed -E 's#(/run-[0-9-]+)$#\1.1#' \
          | sort -t. -k1,1 -k2,2n | sed -E 's#\.1$##')
# keep only the runs from this DLE's most recent full onward:
full=$(for d in $runs; do ls "$d"/0*-"$dle"-L0*.tar* 2>/dev/null; done | tail -1)
chain=$(printf '%s\n' "$runs" | sed -n "\#^$(dirname "$full")\$#,\$p")
for lvl in $(seq 0 9); do
  a=$(for d in $chain; do ls "$d"/0*-"$dle"-L"$lvl"*.tar* 2>/dev/null; done | tail -1)
  [ -n "$a" ] && zstd -dc "$a" | tar --extract --listed-incremental=/dev/null
done

(The L<n>*.tar* glob — a wildcard before .tar, not just after — matches both a plain …-L0.tar.zst and an encrypted …-L0.p000.tar.zst.gpg. An encrypted level still needs the atom loop below in place of the bare zstd -dc "$a" line.)

Encrypted archives

An encrypted archive is always stored as one or more atoms, even when it is a single, unsplit archive: gpg refuses to decrypt concatenated messages, so no encrypted payload can be the plain …-L0.tar.<ext> a full’s name otherwise is. Every part — one in the common case — carries a .pNNN part-index suffix BEFORE the extensions (…-L0.p000.tar.zst.gpg — a valid .gpg file, unlike a slice’s ….tar.zst.gpg.p000), signalling that it is ciphertext. The recipe is always the same file loop — decrypt each atom, concatenate the plaintexts (whole compressed frames: one valid stream), then decompress and untar once — whether there is one atom or many:

# public-key (the private key is in the operator's keyring):
for p in 00*-app01-home-L0.p*.tar.zst.gpg; do gpg -d "$p"; done | zstd -dc | tar -xf -

# symmetric (passphrase_file) — supply the passphrase non-interactively, or a bare
# `gpg -d` blocks on a pinentry prompt:
for p in 00*-app01-home-L0.p*.tar.zst.gpg; do
    gpg --batch --pinentry-mode loopback --passphrase-file /etc/nbackup/secret -d "$p"
done | zstd -dc | tar -xf -

A public-key dump restores on any host with the private key in its keyring; a symmetric (passphrase_file) dump needs the same passphrase supplied to gpg.

The loop needs no index — the boundaries are the file boundaries, and ls | sort already yields part order. Re-cutting an atom needs the key, so copies carry atoms unchanged to every medium; an nb repack (decrypt → re-encrypt at a new atom size) is deliberately not built — retention ages old atom sizes out on its own.

From tape

Tape frames each payload with a fixed 32 KB inline header — skip it first. On an emulated library each volume is a slot-NN/ directory (or, with a bucket-URL dir:, key prefix) of plain files, so a regular dd reads one — download the object first if it lives in a bucket:

dd bs=32k skip=1 < file | zstd -dc | tar -xf -

On a real drive (/dev/nst0) the backend writes in variable-block mode, so position to the file with mt and read with a block size at least the medium’s block_size (the records are that big — bs=256k covers the 256k ceiling):

mt -f /dev/nst0 asf 1                              # position to tape file 1 (file 0 is the label)
dd if=/dev/nst0 bs=256k skip=1 | zstd -dc | tar -xf -   # skip the 32 KB header record

A spanned archive is split into parts written across several volumes. On a dir-backed library each volume is a slot-NN/ directory whose file 000000 is the volume’s identity label; the data files follow as 000001, 000002, …. `nb run

` prints the volume chain — in write order — and, per archive, the file position of each part (a position of `1` is the file `000001`). Match each volume label to its `slot-NN/` directory by reading that directory's `000000` label file. Then read each part as `/slot-NN/`, strip its 32 KB header, and concatenate the parts in chain order before decompressing: ```bash # one part per volume, in the chain order `nb run` prints (positions here are 1): for p in vtape/slot-08/000001 vtape/slot-01/000001 vtape/slot-02/000001; do dd bs=32k skip=1 < "$p" done | zstd -dc | tar -xf - ```