Compare commits

...
43 Commits
Author SHA256 Message Date
spacefreak bc8287a28c only look for version tags 2026-08-24 01:11:01 +02:00
spacefreak 04d351a859 add WSL section to README.md 2026-08-24 01:07:11 +02:00
spacefreak d5d4ea2430 change README.md 2026-08-24 00:53:49 +02:00
spacefreak e423b97c8a add README.md 2026-08-24 00:40:09 +02:00
spacefreak 01c299e242 fail instead of looping when no mount target can be read 2026-08-22 07:45:56 +02:00
spacefreak 56b619372d report PBC query failures instead of an empty datastore 2026-08-22 07:39:16 +02:00
spacefreak 73202f5e3b extract shared snapshot and archive selection logic 2026-08-22 07:36:30 +02:00
spacefreak d243ea1c89 resolve symlinks for SCRIPT_DIR lookup 2026-08-22 06:58:45 +02:00
spacefreak 0623c6a985 remove duplicate ETC_DIR assignment 2026-08-22 06:54:35 +02:00
spacefreak 4fb949c26e declare variable full as local in usage 2026-08-22 06:51:56 +02:00
spacefreak e065ab2cd7 use dedicated variable update_pbc for the install flag 2026-08-22 06:48:59 +02:00
spacefreak 7421c8f43e remove redundant $ inside arithmetic contexts 2026-08-22 06:43:04 +02:00
spacefreak e184d1c92c test may_select directly instead of $? 2026-08-22 06:41:05 +02:00
spacefreak ee86603dcd forward extra arguments to proxmox-backup-client backup 2026-08-22 06:37:37 +02:00
spacefreak 1eee4b2d1d remove no-op return from the pbc-* wrappers 2026-08-22 06:35:53 +02:00
spacefreak 7555745704 install pbc with an explicit mode and owner 2026-08-22 06:26:20 +02:00
spacefreak 49d2e35106 parse gitea tags without jq 2026-08-22 06:24:26 +02:00
spacefreak 9722741bfd check EUID instead of whoami for root 2026-08-22 06:21:31 +02:00
spacefreak 22abaf6431 rename TMPDIR to WORK_DIR 2026-08-22 06:19:51 +02:00
spacefreak 4e24ee218c send usage to stderr on error paths 2026-08-22 06:17:51 +02:00
spacefreak acac5b685b show usage when no command is given 2026-08-22 06:15:59 +02:00
spacefreak 3869ca5a95 install example config with a restrictive mode 2026-08-22 06:12:38 +02:00
spacefreak 3d5757effc check config file permissions before sourcing 2026-08-22 06:10:50 +02:00
spacefreak 6c7db396b2 make apt-get install non-interactive 2026-08-22 06:07:40 +02:00
spacefreak eeb0c9cb47 install client binary instead of cp 2026-08-22 06:06:29 +02:00
spacefreak 44cc111b3d fix version sorting when determining debian pkg to download 2026-08-22 06:03:06 +02:00
spacefreak f0610091bb fail curl on http errors 2026-08-22 06:00:44 +02:00
spacefreak f93f647504 add guard to may_select in case stdin is not a terminal 2026-08-22 05:54:50 +02:00
spacefreak 32e88beedc fix argument iteration 2026-08-22 05:48:23 +02:00
spacefreak 6b2847fec7 make full_usage aware of possible PBC setting in config 2026-08-22 05:46:16 +02:00
spacefreak 127fb83567 fix executable checks 2026-08-22 05:37:25 +02:00
spacefreak bfa15561d8 use local keywords only once for version variable 2026-08-22 05:34:26 +02:00
spacefreak 22675d1be3 fix error handling 2026-08-22 05:31:27 +02:00
spacefreak 62f6865a06 fix pkg install status check with apt-cache and mention pbs-client repos 2026-08-22 05:20:23 +02:00
spacefreak 331f75a943 replace whish -s with command -v 2026-08-22 05:15:25 +02:00
spacefreak d1791cfcc8 fix exit code and output to stderr in backup-cron command 2026-08-22 05:09:15 +02:00
spacefreak 8f83861965 fix bugs in config.example and install.sh 2026-08-22 02:45:04 +02:00
spacefreak e613532322 change help message 2026-08-22 02:37:32 +02:00
spacefreak d54fe81dc5 fix log messages 2026-08-22 02:33:09 +02:00
spacefreak bf54eab7c7 use safe file mode for example config 2026-08-22 02:19:17 +02:00
spacefreak 6a8b4b86f3 improve log messages 2026-08-22 02:12:13 +02:00
spacefreak 37dbf61f07 change error message 2026-08-22 02:05:17 +02:00
spacefreak c245c4fa1e mark variable rc as local 2026-08-22 02:03:58 +02:00
4 changed files with 438 additions and 160 deletions
+272
View File
@@ -0,0 +1,272 @@
# pbc
A Bash wrapper around Proxmox's `proxmox-backup-client` for Proxmox Backup Server (PBS).
It keeps the repository, credentials and the list of paths to back up in a single config
file, injects the configured namespace (`--ns`) into every command that needs it, and lets
you pick a snapshot group, snapshot and archive from an interactive menu instead of typing
them out. Any command it does not implement itself is passed straight through to
`proxmox-backup-client`.
## Requirements
- A Debian-based host with `bash` and `curl`
- `jq` and `proxmox-backup-client` — both installed by `pbc install`
- A reachable PBS with a datastore and an API token
## Installation
```bash
curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.sh | sudo bash
```
This installs the newest tagged version to `/usr/local/bin/pbc`, installs the
dependencies, and puts an example config at `/etc/pbc/config.example`.
### Updating
Run the same command again to update `pbc` itself:
```bash
curl -fsSL https://git.ccc-rheintal.ch/spacefreak/pbc/raw/branch/master/install.sh | sudo bash
```
It fetches the newest tag and overwrites `/usr/local/bin/pbc`. Your `/etc/pbc/config` is
left untouched.
To update only the `proxmox-backup-client` binary:
```bash
sudo pbc install --update
```
This is skipped when the client is managed by the package manager — use `apt` in that case.
## Configuration
```bash
sudo cp /etc/pbc/config.example /etc/pbc/config
sudo chmod 640 /etc/pbc/config
sudo editor /etc/pbc/config
```
```bash
PBS_SERVER='pbs.domain.tld:8007'
PBS_USER='backup@pam:token-name'
PBS_PASSWORD='XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
PBS_DATASTORE='backup'
PBS_NAMESPACE='MyBackups'
BACKUP=('root.pxar:/' 'data.pxar:/mnt/data')
```
| Key | Description |
|---|---|
| `PBS_SERVER` | PBS host and port |
| `PBS_USER` | User and API token name, `user@realm:token-name` |
| `PBS_PASSWORD` | The API token secret |
| `PBS_DATASTORE` | Datastore to back up to |
| `PBS_NAMESPACE` | Namespace inside the datastore |
| `BACKUP` | Array of `archive-name.pxar:/path` entries to back up |
| `PBC` | Optional, path to `proxmox-backup-client` (default `/usr/local/bin/proxmox-backup-client`) |
`pbc` never creates the config for you — copy the example and edit it yourself. The file
has to be readable for the user that runs `pbc`, so if that is not root, hand
it over:
```bash
sudo chown youruser /etc/pbc/config
```
The config is sourced as Bash and contains your token secret. `pbc` refuses to run if the
file is writable by group or others, and warns if it is readable by others.
Check that it works:
```bash
pbc list
```
Use a different config with `-c`:
```bash
pbc -c ./myconfig list
```
## Usage
Run `pbc --help` for the synopsis and the list of options, or `pbc -H` to additionally
list every command of `proxmox-backup-client`. The commands `pbc` implements itself are
described below.
### backup
Back up everything listed in `BACKUP` and print the client output:
```bash
pbc backup
```
Extra arguments are forwarded to the client:
```bash
pbc backup --exclude '/mnt/data/cache'
```
### backup-cron
Same backup, but output is suppressed unless it fails — so cron only mails you on errors:
```cron
0 3 * * * /usr/local/bin/pbc backup-cron
```
### list
List backup groups with their latest snapshot and archives:
```bash
pbc list
```
### snapshot-list
List snapshots of a group with sizes and archives. Without an argument you get a menu of
the available groups:
```bash
pbc snapshot-list
pbc snapshot-list host/myhost
```
### mount
Mount a single archive of a snapshot on a local directory. Anything you leave out is
asked for interactively:
```bash
pbc mount
pbc mount host/myhost/2026-08-24T01:00:00Z root.pxar /mnt/restore
```
The target directory is not created for you — create it beforehand and make sure it is
writable for the user that runs `pbc`. Unmount when done:
```bash
umount /mnt/restore
```
### catalog-shell
Open an interactive shell to browse an archive and restore selected files — see below.
```bash
pbc catalog-shell
pbc catalog-shell host/myhost/2026-08-24T01:00:00Z root.pxar
```
### install
Install or update the dependencies. Requires root:
```bash
sudo pbc install
sudo pbc install --update
```
## Restoring files
### Selectively, with `catalog-shell`
```bash
pbc catalog-shell
```
Pick a snapshot and an archive from the menu, then browse the archive as if it were a
filesystem and mark what you want back:
```
pxar:/ > cd etc
pxar:/etc > ls
pxar:/etc > select hosts
pxar:/etc > find etc/nginx/** --select
pxar:/etc > list-selected
pxar:/etc > restore-selected /mnt/restore
pxar:/etc > exit
```
- `ls`, `cd`, `pwd`, `stat` — browse the archive
- `select <path>` — mark a single file, relative to the current directory
- `find <pattern> --select` — mark everything matching a glob
- `deselect <path>` / `clear-selected` — drop one entry or all of them
- `list-selected` — show what is currently marked
- `restore-selected <target>` — restore only the marked entries
- `restore <target> [pattern]` — restore the current directory and everything below it
- `help` — list all available shell commands
- `exit` — leave the shell
#### Selecting a directory and everything beneath it
`select` marks exactly the entry you give it and nothing else. Marking a directory
therefore restores an empty directory — its contents are *not* included. Use a glob with
`find --select` instead:
```
pxar:/ > find etc/nginx/** --select
pxar:/ > select etc/nginx
pxar:/ > restore-selected /mnt/restore
```
The `find` line picks up everything below `etc/nginx`; the `select` line adds the
directory itself, so its own permissions and ownership are restored as well.
Note that `find` patterns are always matched against paths relative to the archive root,
no matter which directory you are in, and that `find` scans the whole archive — expect it
to take a while on large backups.
The target path must not exist yet; the restore creates it. Its parent directory has to be
writable for the user that runs `pbc`.
### Everything, or with normal tools
To copy files out with `cp`, `rsync` or a file manager, mount the archive instead:
```bash
mkdir -p /mnt/restore # must exist and be writable for the user running pbc
pbc mount
cp -a /mnt/restore/etc/nginx /etc/nginx
umount /mnt/restore
```
## Running in WSL
`pbc` also works inside a Debian-based WSL instance, which makes it a way to back up
Windows directories: the Windows drives show up under `/mnt`, so they can be listed in
`BACKUP` like any other path.
```bash
BACKUP=('users.pxar:/mnt/c/Users' 'projects.pxar:/mnt/d/projects')
```
A few things to keep in mind:
- Access to files under `/mnt` is governed by Windows, not by the user you are inside the
WSL instance — `sudo` does not help there. To back up paths your Windows user cannot
read, start the WSL instance itself as administrator (run the terminal or `wsl.exe` via
*Run as administrator*), then run `pbc` in it.
- The owner and permission metadata stored in the archive is the one WSL synthesizes for
Windows files, not the original Windows ACLs.
- Cron is not running in a WSL instance by default, so `backup-cron` only fires if you
enable it — either by starting `cron` yourself, or by triggering `pbc backup-cron` from
the Windows Task Scheduler with `wsl.exe`:
```
wsl.exe -d Debian -u root /usr/local/bin/pbc backup-cron
```
For the reason above, such a task has to run with highest privileges to reach files that
are not accessible to your Windows user.
## License
GPLv3 — see [LICENSE](LICENSE).
+1 -1
View File
@@ -4,4 +4,4 @@ PBS_PASSWORD='XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
PBS_DATASTORE='backup' PBS_DATASTORE='backup'
PBS_NAMESPACE='MyBackups' PBS_NAMESPACE='MyBackups'
BACKUP=['root.pxar:/', 'data.pxar:/mnt/data'] BACKUP=('root.pxar:/' 'data.pxar:/mnt/data')
+16 -15
View File
@@ -1,51 +1,52 @@
#!/usr/bin/env bash #!/usr/bin/env bash
[ "$(whoami)" != "root" ] && echo "Error: installation only works as root!" >&2 && exit 1 (( EUID != 0 )) && echo "Error: installation only works as root!" >&2 && exit 1
API_ENDPOINT="https://git.ccc-rheintal.ch/api/v1/repos/spacefreak/pbc" API_ENDPOINT="https://git.ccc-rheintal.ch/api/v1/repos/spacefreak/pbc"
TMPDIR='' WORK_DIR=''
function cleanup_tmpdir() { function cleanup_work_dir() {
[ -n "$TMPDIR" -a -d "$TMPDIR" ] && rm -r "$TMPDIR" [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR"
trap - EXIT trap - EXIT
} }
function download_files() { function download_files() {
local path local path rc
local query_params='' local query_params=''
[ -n "$GIT_TAG" ] && query_params="?ref=$GIT_TAG" [ -n "$GIT_TAG" ] && query_params="?ref=$GIT_TAG"
while [ -n "$1" ]; do while (( $# > 0 )); do
path=$1 path=$1
shift shift
curl -s --output-dir "$TMPDIR/" --remote-name "$API_ENDPOINT/raw/$path$query_params" curl -fsSL --output-dir "$WORK_DIR/" --remote-name "$API_ENDPOINT/raw/$path$query_params"
rc=$? rc=$?
if (( $rc != 0 )); then if (( rc != 0 )); then
return $rc return $rc
fi fi
done done
} }
GIT_TAG=$(curl -s "$API_ENDPOINT/tags" | jq -r '.[] | .name' | sort --version-sort | tail -n 1) GIT_TAG=$(curl -fsSL "$API_ENDPOINT/tags" | grep -o '{"name":"[^"]*"' | sed 's/^{"name":"//;s/"$//' | grep -E '^v[0-9]+' | sort --version-sort | tail -n 1)
if [ -n "$GIT_TAG" ]; then if [ -n "$GIT_TAG" ]; then
echo "Info: installing pbc version $GIT_TAG" echo "Info: installing pbc version $GIT_TAG"
else else
echo "Warning: no Git tag found, installing from repository default branch" >&2 echo "Warning: no Git tag found, installing from repository default branch" >&2
fi fi
TMPDIR=$(mktemp -d) WORK_DIR=$(mktemp -d)
trap cleanup_tmpdir EXIT trap cleanup_work_dir EXIT
! download_files config.example pbc && echo "Error downloading files!" >&2 && exit 10 ! download_files config.example pbc && echo "Error downloading files!" >&2 && exit 10
chmod +x "$TMPDIR/pbc" chmod +x "$WORK_DIR/pbc"
chmod o= "$WORK_DIR/config.example"
"$TMPDIR/pbc" install "$WORK_DIR/pbc" install
rc=$? rc=$?
if (( $? != 0 )); then if (( rc != 0 )); then
echo "Error installing pbc requirements!" >&2 echo "Error installing pbc requirements!" >&2
exit $rc exit $rc
fi fi
! mv "$TMPDIR/pbc" /usr/local/bin/pbc && echo 'Error moving pbc to /usr/local/bin/pbc !' >&2 && exit 20 ! install -m 0755 -o root -g root "$WORK_DIR/pbc" /usr/local/bin/pbc && echo 'Error installing file!' >&2 && exit 20
echo "Success!" echo "Success!"
+149 -144
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail
function usage() { function usage() {
full=${1:-false} local full=${1:-false}
cat <<EOF cat <<EOF
Usage: $SCRIPT [-h] [-H] [-c config] command [OPTIONS] Usage: $SCRIPT [-h] [-H] [-c config] command [OPTIONS]
@@ -22,8 +24,7 @@ snapshot-list [group] list snapshots, sizes and archives
catalog-shell [snapshot [archive]] run interactive shell to explore arhive contents catalog-shell [snapshot [archive]] run interactive shell to explore arhive contents
and restore files/dirs selectively and restore files/dirs selectively
mount [snapshot [archive [target]]] mount snapshot archive locally mount [snapshot [archive [target]]] mount snapshot archive locally
install [--update] install proxmox-backup-client binary and dependencies install [--update] install/update proxmox-backup-client and pdb dependencies
use --update to update the currently installed binary
EOF EOF
! $full && cat <<EOF ! $full && cat <<EOF
@@ -35,53 +36,34 @@ EOF
function full_usage() { function full_usage() {
usage true usage true
[ ! -e "$PBC" ] && echo "Error: $PBC is not executable!" >&2 && return 254 [ ! -x "$PBC" ] && echo "Error: $PBC is not executable!" >&2 && return 254
echo "Commands of proxmox-backup-client:" echo "Commands of proxmox-backup-client:"
echo echo
"$PBC" help | tail -n +3 "$PBC" help | tail -n +3
echo
return $?
} }
function pbc-version() { function pbc-version() {
local vers_info=$("$PBC" version) local vers_info rc
local rc=$? vers_info=$("$PBC" version)
(( $rc != 0 )) && echo "Error running $PBC version ... exited with rc=$rc" >&2 && return $rc rc=$?
(( rc != 0 )) && echo "Error running $PBC version ... exited with rc=$rc" >&2 && return $rc
echo "$vers_info" | grep client echo "$vers_info" | grep client
rc=$? rc=$?
(( $rc != 0 )) && echo "Error reading client version, output was:" >&2 && echo "$vers_info" >&2 (( rc != 0 )) && echo "Error reading client version, output was:" >&2 && echo "$vers_info" >&2
return $rc return $rc
} }
function pbc-backup() { function pbc-backup() { "$PBC" backup --ns "$PBS_NAMESPACE" "${BACKUP[@]}" "$@"; }
"$PBC" backup --ns "$PBS_NAMESPACE" "${BACKUP[@]}"
return $?
}
function pbc-list() { function pbc-list() { "$PBC" list --ns "$PBS_NAMESPACE" "$@"; }
"$PBC" list --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-snapshot-list() { function pbc-snapshot-list() { "$PBC" snapshot list --ns "$PBS_NAMESPACE" "$@"; }
"$PBC" snapshot list --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-files() { function pbc-files() { "$PBC" files --ns "$PBS_NAMESPACE" "$@"; }
"$PBC" files --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-catalog-shell() { function pbc-catalog-shell() { "$PBC" catalog shell --ns "$PBS_NAMESPACE" "$@"; }
"$PBC" catalog shell --ns "$PBS_NAMESPACE" "$@"
return $?
}
function pbc-mount() { function pbc-mount() { "$PBC" mount --ns "$PBS_NAMESPACE" "$@"; }
"$PBC" mount --ns "$PBS_NAMESPACE" "$@"
return $?
}
function get_group_names() { function get_group_names() {
pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"' pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"'
@@ -98,64 +80,67 @@ function get_snapshot_files() {
function apt_get_install() { function apt_get_install() {
local package=$1 local package=$1
echo "Installing $package using package manager ..." echo "Installing $package using package manager ..."
apt-get install "$package" >/dev/null DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$package" >/dev/null
local rc=$? local rc=$?
(( $rc != 0 )) && echo "Error installing package!" >&2 (( rc != 0 )) && echo "Error installing package!" >&2
return $rc return $rc
} }
TMPDIR='' WORK_DIR=''
function cleanup_tmpdir() { function cleanup_work_dir() {
[ -n "$TMPDIR" -a -d "$TMPDIR" ] && rm -r "$TMPDIR" [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR"
trap - EXIT trap - EXIT
} }
function install_proxmox_backup_client() { function install_proxmox_backup_client() {
local update=${1:-false} local update=${1:-false}
local version
[ -f "$PBC" ] && local installed=true || local installed=false [ -f "$PBC" ] && local installed=true || local installed=false
! $installed && update=false ! $installed && update=false
if $installed && ! $update; then if $installed && ! $update; then
local version=$("$PBC" version | head -n 1) version=$(pbc-version) || return 10
(( $? != 0 )) && echo "Error running $PBC version: exited with rc=$?" >&2 && return 10 echo "Info: skip installation of proxmox-backup-client, already installed ($version)"
echo "Skip installation of proxmox-backup-client, already installed ($version)"
return 0 return 0
fi fi
if $update && dpkg -S "$PBC" &>/dev/null; then if $update && dpkg -S "$PBC" &>/dev/null; then
echo "Skip update of proxmox-backup-client, package is managed by the package manager" echo "Info: skip update of proxmox-backup-client, package is managed by the package manager"
return 0 return 0
fi fi
if apt-get search proxmox-backup-client &>/dev/null; then if apt-cache show proxmox-backup-client &>/dev/null; then
apt_get_install proxmox-backup-client apt_get_install proxmox-backup-client
return $? return $?
fi fi
echo "Info: proxmox-backup-client is not available from any configured apt repository"
echo "Info: add the Proxmox pbs-client repository to install a signed, apt-managed package"
$update && local action='Updating' || local action='Installing' $update && local action='Updating' || local action='Installing'
echo "$action proxmox-backup-client-static binary ..." echo "$action proxmox-backup-client-static binary ..."
local pkgfile=$(curl -s "$PBC_DOWNLOAD_URL/" | grep proxmox-backup-client-static_ | tail -n 1 | sed 's#.*href="##g;s#".*##g;') local pkgfile=$(curl -fsSL "$PBC_DOWNLOAD_URL/" | grep proxmox-backup-client-static_ | sed 's#.*href="##g;s#".*##g;' | sort --version-sort | tail -n 1)
[ -z "$pkgfile" ] && echo "Unable to determine current package file!" >&2 && return 30 [ -z "$pkgfile" ] && echo "Unable to determine current package file!" >&2 && return 30
TMPDIR=$(mktemp -d) WORK_DIR=$(mktemp -d)
trap cleanup_tmpdir EXIT trap cleanup_work_dir EXIT
echo "Downloading $PBC_DOWNLOAD_URL/$pkgfile ..." echo "Info: downloading $PBC_DOWNLOAD_URL/$pkgfile ..."
! curl -s -o "$TMPDIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40 ! curl -fsSL -o "$WORK_DIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40
echo "Extracting $pkgfile ..." echo "Info: extracting $pkgfile ..."
! dpkg-deb -x "$TMPDIR/$pkgfile" "$TMPDIR/" && echo "Error extracting file!" >&2 && return 50 ! dpkg-deb -x "$WORK_DIR/$pkgfile" "$WORK_DIR/" && echo "Error extracting file!" >&2 && return 50
echo "Copying proxmox-backup-client static binary to $PBC ..." echo "Info: installing proxmox-backup-client static binary to $PBC ..."
! cp "$TMPDIR/usr/bin/proxmox-backup-client" "$PBC" && echo "Error copying binary!" >&2 && return 60 ! install -m 0755 -D "$WORK_DIR/usr/bin/proxmox-backup-client" "$PBC" && echo "Error installing binary!" >&2 && return 60
local version=$(pbc-version) || return $? version=$(pbc-version) || return $?
$update && action='updated' || action='installed' $update && action='updated' || action='installed'
echo "Successfully $action proxmox-backup-$version" echo "Info: successfully $action proxmox-backup-$version"
cleanup_tmpdir cleanup_work_dir
} }
@@ -163,7 +148,7 @@ function arr_contains() {
local search=$1 local search=$1
shift shift
[ -z "$search" ] && return 1 [ -z "$search" ] && return 1
while [ -n "$1" ]; do while (( $# > 0 )); do
[ "$1" == "$search" ] && return 0 [ "$1" == "$search" ] && return 0
shift shift
done done
@@ -179,8 +164,12 @@ function may_select() {
if [ -z "$value" ]; then if [ -z "$value" ]; then
if (( $# == 1 )); then if (( $# == 1 )); then
value=$1 value=$1
elif [ ! -t 0 ]; then
echo "Error: no value given and stdin is not a terminal" >&2
return 1
else else
local sorted=($(printf "%s\n" "$@" | sort)) local sorted
readarray -t sorted < <(printf '%s\n' "$@" | sort)
select value in "${sorted[@]}"; do select value in "${sorted[@]}"; do
[ -n "$value" ] && break [ -n "$value" ] && break
done done
@@ -189,24 +178,54 @@ function may_select() {
return 1 return 1
fi fi
[ -z "$value" ] && return 1
echo "$value" echo "$value"
return 0 }
function select_group() {
local groups raw
raw=$(get_group_names) || { echo "Error: failed to query PBS!" >&2; return 1; }
[ -z "$raw" ] && return 2
readarray -t groups <<< "$raw"
PS3="Snapshot group [1-${#groups[@]}]: "
GROUP=$(may_select "$1" "${groups[@]}") || { echo "Error: invalid group" >&2; return 1; }
}
function select_snapshot_and_archive() {
local snapshots archives raw
raw=$(get_snapshot_names) || { echo "Error: failed to query PBS!" >&2; return 1; }
[ -z "$raw" ] && return 2
readarray -t snapshots <<< "$raw"
PS3="Snapshot [1-${#snapshots[@]}]: "
SNAPSHOT=$(may_select "$1" "${snapshots[@]}") || { echo "Error: invalid snapshot" >&2; return 1; }
raw=$(get_snapshot_files "$SNAPSHOT") || { echo "Error: failed to query PBS!" >&2; return 1; }
[ -z "$raw" ] && return 3
readarray -t archives <<< "$raw"
PS3="Archive [1-${#archives[@]}]: "
ARCHIVE=$(may_select "$2" "${archives[@]}") || { echo "Error: invalid archive" >&2; return 1; }
} }
SCRIPT_PATH=$(realpath -s "$0") SCRIPT_PATH=$(realpath -s "$0")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH") SCRIPT_DIR=$(dirname "$(realpath "$0")")
SCRIPT=$(basename "$SCRIPT_PATH") SCRIPT=$(basename "$SCRIPT_PATH")
PBC=$(which proxmox-backup-client) PBC=$(command -v proxmox-backup-client)
PBC_DOWNLOAD_URL='http://download.proxmox.com/debian/pbs-client/dists/trixie/main/binary-amd64' PBC_DOWNLOAD_URL='http://download.proxmox.com/debian/pbs-client/dists/trixie/main/binary-amd64'
ETC_DIR="/etc/$SCRIPT" ETC_DIR="/etc/$SCRIPT"
full_help=false
update_pbc=false
action='' action=''
cfgfile="$ETC_DIR/config" cfgfile="$ETC_DIR/config"
args=() args=()
while [ -n "$1" ]; do while (( $# > 0 )); do
opt=$1 opt=$1
shift shift
if [ -z "$action" ]; then if [ -z "$action" ]; then
@@ -214,20 +233,19 @@ while [ -n "$1" ]; do
-c|--config) -c|--config)
cfgfile=$1 cfgfile=$1
shift shift
[ -z "$cfgfile" ] && usage && exit 255 [ -z "$cfgfile" ] && echo "Error: missing argument for $opt" >&2 && usage >&2 && exit 255
;; ;;
-h|--help) -h|--help)
usage usage
exit 0 exit 0
;; ;;
-H|--full-help) -H|--full-help)
full_usage full_help=true
exit 0
;; ;;
-*) -*)
echo "Error: invalid option: $opt" >&2 echo "Error: invalid option: $opt" >&2
echo echo >&2
usage usage >&2
exit 255 exit 255
;; ;;
*) *)
@@ -237,18 +255,18 @@ while [ -n "$1" ]; do
elif [ "$action" == "install" ]; then elif [ "$action" == "install" ]; then
case "$opt" in case "$opt" in
-u|--update) -u|--update)
args+=('true') update_pbc=true
;; ;;
-*) -*)
echo "Error: invalid option: $opt" >&2 echo "Error: invalid option: $opt" >&2
echo echo >&2
usage usage >&2
exit 255 exit 255
;; ;;
*) *)
echo "Error: invalid argument: $opt" >&2 echo "Error: invalid argument: $opt" >&2
echo echo >&2
usage usage >&2
exit 255 exit 255
;; ;;
esac esac
@@ -258,103 +276,94 @@ while [ -n "$1" ]; do
done done
if [ -f "$cfgfile" ]; then
perms=$(stat -c '%a' "$cfgfile")
(( 8#$perms & 0022 )) && echo "Error: $cfgfile is writable by group or others!" >&2 && exit 202
(( 8#$perms & 0007 )) && echo "Warning: $cfgfile is readable by others!" >&2
fi
if $full_help; then
[ -f "$cfgfile" ] && source "$cfgfile"
PBC=${PBC:-/usr/local/bin/proxmox-backup-client}
full_usage
exit $?
fi
[ -z "$action" ] && usage >&2 && exit 255
if [ "$action" == 'install' ]; then if [ "$action" == 'install' ]; then
if [ -f "$cfgfile" ]; then if [ -f "$cfgfile" ]; then
source "$cfgfile" || exit $? source "$cfgfile" || exit $?
fi fi
PBC=${PBC:-/usr/local/bin/proxmox-backup-client} PBC=${PBC:-/usr/local/bin/proxmox-backup-client}
for exe in apt-get dpkg dpkg-deb curl; do for exe in apt-cache apt-get dpkg dpkg-deb curl; do
! which -s "$exe" && echo "Error: $exe executable not found!" >&2 && exit 200 ! command -v "$exe" >/dev/null && echo "Error: $exe executable not found!" >&2 && exit 200
done done
else else
[ ! -f "$cfgfile" ] && echo "Error: $cfgfile: no such file" >&2 && exit 201
[ ! -r "$cfgfile" ] && echo "Error: $cfgfile: not readable" >&2 && exit 201
source "$cfgfile" || exit $? source "$cfgfile" || exit $?
PBC=${PBC:-/usr/local/bin/proxmox-backup-client} PBC=${PBC:-/usr/local/bin/proxmox-backup-client}
[ ! -f "$PBC" ] && echo "Error: $PBC: no such file" >&2 && exit 200 [ ! -f "$PBC" ] && echo "Error: $PBC: no such file" >&2 && exit 200
[ ! -e "$PBC" ] && echo "Error: $PBC: not executable" >&2 && exit 200 [ ! -x "$PBC" ] && echo "Error: $PBC: not executable" >&2 && exit 200
! which -s jq && echo "Error: jq executable not found!" >&2 && exit 200 ! command -v jq >/dev/null && echo "Error: jq executable not found!" >&2 && exit 200
fi fi
export PBS_REPOSITORY="$PBS_USER@${PBS_SERVER:-8007}:$PBS_DATASTORE" export PBS_REPOSITORY="$PBS_USER@$PBS_SERVER:$PBS_DATASTORE"
export PBS_PASSWORD="$PBS_PASSWORD" export PBS_PASSWORD="$PBS_PASSWORD"
case "$action" in case "$action" in
backup) backup)
pbc-backup || exit $? pbc-backup "${args[@]}" || exit $?
;; ;;
backup-cron) backup-cron)
output=$(pbc-backup 2>&1) output=$(pbc-backup "${args[@]}" 2>&1)
rc=$? rc=$?
(( $rc != 0 )) && echo "$output" (( rc != 0 )) && echo "$output" >&2
exit $rc
;; ;;
list) list)
pbc-list "${args[@]}" || exit $? pbc-list "${args[@]}" || exit $?
;; ;;
snapshot-list) snapshot-list)
readarray -t groups < <(get_group_names) select_group "${args[0]}"
(( ${#groups[@]} == 0 )) && echo "no snapshot groups found" && exit 0 case $? in
0) ;;
group=${args[0]} 2) echo "no snapshot groups found"; exit 0 ;;
args=("${args[@]:1}") *) exit 1 ;;
esac
PS3="Snapshot group [1-${#groups[@]}]: "
group=$(may_select "$group" "${groups[@]}")
(( $? != 0 )) && echo "Error: invalid group" >&2 && exit 1
echo echo
pbc-snapshot-list "$group" "${args[@]}" || exit $? pbc-snapshot-list "$GROUP" "${args[@]:1}" || exit $?
;; ;;
catalog-shell) catalog-shell)
readarray -t snapshots < <(get_snapshot_names) select_snapshot_and_archive "${args[0]}" "${args[1]}"
(( ${#snapshots[@]} == 0 )) && echo "no snapshots found" && exit 0 case $? in
0) ;;
snapshot=${args[0]} 2) echo "no snapshots found"; exit 0 ;;
args=("${args[@]:1}") 3) echo "no archives found"; exit 0 ;;
*) exit 1 ;;
PS3="Snapshot [1-${#snapshots[@]}]: " esac
snapshot=$(may_select "$snapshot" "${snapshots[@]}")
(( $? != 0 )) && echo "Error: invalid snapshot" >&2 && exit 1
readarray -t archives < <(get_snapshot_files "$snapshot")
(( ${#archives[@]} == 0 )) && echo "no archives found" && exit 0
archive=${args[0]}
args=("${args[@]:1}")
PS3="Archive [1-${#archives[@]}]: "
archive=$(may_select "$archive" "${archives[@]}")
(( $? != 0 )) && echo "Error: invalid archive" >&2 && exit 1
echo echo
pbc-catalog-shell "$snapshot" "$archive" || exit $? pbc-catalog-shell "$SNAPSHOT" "$ARCHIVE" || exit $?
;; ;;
mount) mount)
readarray -t snapshots < <(get_snapshot_names) select_snapshot_and_archive "${args[0]}" "${args[1]}"
(( ${#snapshots[@]} == 0 )) && echo "no snapshots found" && exit 0 case $? in
0) ;;
2) echo "no snapshots found"; exit 0 ;;
3) echo "no archives found"; exit 0 ;;
*) exit 1 ;;
esac
snapshot=${args[0]} target=${args[2]}
args=("${args[@]:1}")
PS3="Snapshot [1-${#snapshots[@]}]: "
snapshot=$(may_select "$snapshot" "${snapshots[@]}")
(( $? != 0 )) && echo "Error: invalid snapshot" >&2 && exit 1
readarray -t archives < <(get_snapshot_files "$snapshot")
(( ${#archives[@]} == 0 )) && echo "no archives found" && exit 0
archive=${args[0]}
args=("${args[@]:1}")
PS3="Archive [1-${#archives[@]}]: "
archive=$(may_select "$archive" "${archives[@]}")
(( $? != 0 )) && echo "Error: invalid archive" >&2 && exit 1
target=${args[0]}
args=("${args[@]:1}")
if [ -z "$target" ]; then if [ -z "$target" ]; then
[ ! -t 0 ] && echo "Error: no target path given and stdin is not a terminal" >&2 && exit 1
while [ -z "$target" ]; do while [ -z "$target" ]; do
echo -n "Target path: " echo -n "Target path: "
read -r target ! read -r target && [ -z "$target" ] && echo "Error: no target path given" >&2 && exit 1
[ -d "$target" ] && break [ -d "$target" ] && break
echo -e "\nError: $target: no such directory\n" >&2 echo -e "\nError: $target: no such directory\n" >&2
target='' target=''
@@ -365,36 +374,32 @@ case "$action" in
fi fi
echo echo
pbc-mount "$snapshot" "$archive" "$target" || exit $? pbc-mount "$SNAPSHOT" "$ARCHIVE" "$target" || exit $?
;; ;;
install) install)
update=${args[0]:-false} (( EUID != 0 )) && echo "Error: installation only works as root!" >&2 && exit 1
[ "$(whoami)" != "root" ] && echo "Error: installation only works as root!" >&2 && exit 1 install_proxmox_backup_client "$update_pbc" || exit $?
install_proxmox_backup_client "$update" || exit $?
if dpkg -s jq &>/dev/null; then if dpkg -s jq &>/dev/null; then
$update && echo "Skip update of jq, package is managed by package manager" || echo "Skip installation of jq, already installed" $update_pbc && echo "Info: skip update of jq, package is managed by package manager" || echo "Info: jq is already installed"
else else
apt_get_install jq || exit $? apt_get_install jq || exit $?
fi fi
ETC_DIR="/etc/$SCRIPT"
if [ ! -d "$ETC_DIR" ]; then if [ ! -d "$ETC_DIR" ]; then
echo "Creating config directory $ETC_DIR ..." echo "Creating config directory $ETC_DIR ..."
! mkdir -p "$ETC_DIR" && echo "Error creating directory!" >&2 && exit 3 ! mkdir -p "$ETC_DIR" && echo "Error creating directory!" >&2 && exit 3
fi fi
if [ -f "$SCRIPT_DIR/config.example" ]; then if [ -f "$SCRIPT_DIR/config.example" ]; then
if $update || [ ! -f "$ETC_DIR/config.example" ]; then if $update_pbc || [ ! -f "$ETC_DIR/config.example" ]; then
echo "Copy example config to $ETC_DIR/config.example ..." echo "Install example config to $ETC_DIR/config.example ..."
! cp "$SCRIPT_DIR/config.example" "$ETC_DIR/config.example" && echo "Error copying file!" >&2 && exit 4 ! install -m 0640 "$SCRIPT_DIR/config.example" "$ETC_DIR/config.example" && echo "Error installing file!" >&2 && exit 4
fi fi
fi fi
echo "Success!" echo "Info: successfully installed pbc dependencies"
;; ;;
*) *)
"$PBC" "$action" "${args[@]}" || exit $? "$PBC" "$action" "${args[@]}" || exit $?