2026-04-13 14:49:48 +02:00
#!/bin/bash
# ==========================================================
# ProxMenux - Host Config Backup/Restore - Shared Library
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
2026-05-09 18:59:59 +02:00
# License : GPL-3.0
2026-04-13 14:49:48 +02:00
# Version : 1.0
# Last Updated: 08/04/2026
# ==========================================================
# Do not execute directly — source from backup_host.sh
# Library guard
[ [ " ${ BASH_SOURCE [0] } " = = " $0 " ] ] && {
echo "This file is a library. Source it, do not run it directly." >& 2; exit 1
}
HB_STATE_DIR = "/usr/local/share/proxmenux"
HB_BORG_VERSION = "1.2.8"
HB_BORG_LINUX64_SHA256 = "cfa50fb704a93d3a4fa258120966345fddb394f960dca7c47fcb774d0172f40b"
HB_BORG_LINUX64_URL = " https://github.com/borgbackup/borg/releases/download/ ${ HB_BORG_VERSION } /borg-linux64 "
# Translation wrapper — safe fallback if translate not yet loaded
hb_translate( ) {
declare -f translate >/dev/null 2>& 1 && translate " $1 " || echo " $1 "
}
# ==========================================================
# UI SIZE CONSTANTS
# ==========================================================
HB_UI_MENU_H = 22
HB_UI_MENU_W = 84
HB_UI_MENU_LIST = 10
HB_UI_INPUT_H = 10
HB_UI_INPUT_W = 72
HB_UI_PASS_H = 10
HB_UI_PASS_W = 72
HB_UI_YESNO_H = 10
HB_UI_YESNO_W = 78
# ==========================================================
# DEFAULT PROFILE PATHS
# ==========================================================
hb_default_profile_paths( ) {
local paths = (
"/etc/pve"
"/etc/network"
"/etc/hosts"
"/etc/hostname"
"/etc/ssh"
"/etc/systemd/system"
"/etc/modules"
"/etc/modules-load.d"
"/etc/modprobe.d"
"/etc/udev/rules.d"
"/etc/default/grub"
"/etc/fstab"
"/etc/kernel"
"/etc/apt"
"/etc/vzdump.conf"
"/etc/postfix"
"/etc/resolv.conf"
"/etc/timezone"
"/etc/iscsi"
"/etc/multipath"
"/usr/local/bin"
"/usr/local/share/proxmenux"
"/root"
"/etc/cron.d"
"/etc/cron.daily"
"/etc/cron.hourly"
"/etc/cron.weekly"
"/etc/cron.monthly"
"/etc/cron.allow"
"/etc/cron.deny"
"/var/spool/cron/crontabs"
"/var/lib/pve-cluster"
)
if [ [ -d /etc/zfs ] ] || command -v zpool >/dev/null 2>& 1; then
paths += ( "/etc/zfs" )
fi
printf '%s\n' " ${ paths [@] } "
}
# ==========================================================
# PATH CLASSIFICATION (restore safety)
# Returns: dangerous | reboot | hot
# ==========================================================
hb_classify_path( ) {
local rel = " $1 " # without leading /
case " $rel " in
etc/pve| etc/pve/*| \
var/lib/pve-cluster| var/lib/pve-cluster/*| \
etc/network| etc/network/*)
echo "dangerous" ; ;
etc/modules| etc/modules/*| \
etc/modules-load.d| etc/modules-load.d/*| \
etc/modprobe.d| etc/modprobe.d/*| \
etc/udev/rules.d| etc/udev/rules.d/*| \
etc/default/grub| \
etc/fstab| \
etc/kernel| etc/kernel/*| \
etc/iscsi| etc/iscsi/*| \
etc/multipath| etc/multipath/*| \
etc/zfs| etc/zfs/*)
echo "reboot" ; ;
*)
echo "hot" ; ;
esac
}
hb_path_warning( ) {
local rel = " $1 "
case " $rel " in
etc/pve| etc/pve/*)
hb_translate "/etc/pve is managed by pmxcfs (cluster filesystem). Applying this on a running node can corrupt cluster state. Use 'Export to file' and apply it manually during a maintenance window." ; ;
var/lib/pve-cluster| var/lib/pve-cluster/*)
hb_translate "/var/lib/pve-cluster is live cluster data. Never restore this while the node is running. Use 'Export to file' for manual recovery only." ; ;
etc/network| etc/network/*)
hb_translate "/etc/network controls active interfaces. Applying may immediately change or drop network connectivity, including active SSH sessions." ; ;
esac
}
# ==========================================================
# PROFILE PATH SELECTION
# ==========================================================
hb_select_profile_paths( ) {
local mode = " $1 "
local __out_var = " $2 "
local -n __out_ref = " $__out_var "
mapfile -t __defaults < <( hb_default_profile_paths)
if [ [ " $mode " = = "default" ] ] ; then
__out_ref = ( " ${ __defaults [@] } " )
return 0
fi
local options = ( ) idx = 1 path
for path in " ${ __defaults [@] } " ; do
options += ( " $idx " " $path " "off" )
( ( idx++) )
done
local selected
selected = $( dialog --backtitle "ProxMenux" \
--title " $( hb_translate "Custom backup profile" ) " \
--separate-output --checklist \
" $( hb_translate "Select paths to include:" ) " \
26 86 18 " ${ options [@] } " 3>& 1 1>& 2 2>& 3) || return 1
__out_ref = ( )
local choice
while read -r choice; do
[ [ -z " $choice " ] ] && continue
__out_ref += ( " ${ __defaults [ $(( choice-1)) ] } " )
done <<< " $selected "
if [ [ ${# __out_ref [@] } -eq 0 ] ] ; then
dialog --backtitle "ProxMenux" --title " $( hb_translate "Error" ) " \
--msgbox " $( hb_translate "No paths selected. Select at least one path." ) " 8 60
return 1
fi
}
# ==========================================================
# STAGING OPERATIONS
# ==========================================================
hb_prepare_staging( ) {
local staging_root = " $1 " ; shift
local paths = ( " $@ " )
rm -rf " $staging_root "
mkdir -p " $staging_root /rootfs " " $staging_root /metadata "
local selected_file = " $staging_root /metadata/selected_paths.txt "
local missing_file = " $staging_root /metadata/missing_paths.txt "
: > " $selected_file "
: > " $missing_file "
local p rel target
for p in " ${ paths [@] } " ; do
rel = " ${ p #/ } "
echo " $rel " >> " $selected_file "
[ [ -e " $p " ] ] || { echo " $p " >> " $missing_file " ; continue ; }
target = " $staging_root /rootfs/ $rel "
if [ [ -d " $p " ] ] ; then
mkdir -p " $target "
local -a rsync_opts = (
-aAXH --numeric-ids
--exclude "images/"
--exclude "dump/"
--exclude "tmp/"
--exclude "*.log"
)
# /root is included by default for easier recovery, but avoid volatile/sensitive noise.
if [ [ " $rel " = = "root" || " $rel " = = "root/" * ] ] ; then
rsync_opts += (
--exclude ".bash_history"
--exclude ".cache/"
--exclude "tmp/"
--exclude ".local/share/Trash/"
)
fi
# Runtime pending-restore data belongs in /var/lib/proxmenux, never in app code tree.
if [ [ " $rel " = = "usr/local/share/proxmenux" || " $rel " = = "usr/local/share/proxmenux/" * ] ] ; then
rsync_opts += (
--exclude "restore-pending/"
)
fi
rsync " ${ rsync_opts [@] } " " $p / " " $target / " 2>/dev/null || true
else
mkdir -p " $( dirname " $target " ) "
cp -a " $p " " $target " 2>/dev/null || true
fi
done
# Metadata snapshot
local meta = " $staging_root /metadata "
{
echo " generated_at= $( date -Iseconds) "
echo " hostname= $( hostname) "
echo " kernel= $( uname -r) "
} > " $meta /run_info.env "
command -v pveversion >/dev/null 2>& 1 && pveversion -v > " $meta /pveversion.txt " 2>& 1 || true
command -v lsblk >/dev/null 2>& 1 && lsblk -f > " $meta /lsblk.txt " 2>& 1 || true
command -v qm >/dev/null 2>& 1 && qm list > " $meta /qm-list.txt " 2>& 1 || true
command -v pct >/dev/null 2>& 1 && pct list > " $meta /pct-list.txt " 2>& 1 || true
command -v zpool >/dev/null 2>& 1 && zpool status > " $meta /zpool.txt " 2>& 1 || true
# Manifest + checksums
(
cd " $staging_root /rootfs " || return 1
find . -mindepth 1 -print | sort > " $meta /manifest.txt "
find . -type f -print0 | sort -z | xargs -0 sha256sum 2>/dev/null \
> " $meta /checksums.sha256 " || true
)
}
hb_load_restore_paths( ) {
local restore_root = " $1 "
local __out_var = " $2 "
local -n __out = " $__out_var "
__out = ( )
local selected = " $restore_root /metadata/selected_paths.txt "
if [ [ -f " $selected " ] ] ; then
while IFS = read -r line; do
[ [ -n " $line " ] ] && __out += ( " $line " )
done < " $selected "
fi
# Fallback: scan rootfs
if [ [ ${# __out [@] } -eq 0 ] ] ; then
local p
while IFS = read -r p; do
[ [ -n " $p " && -e " $restore_root /rootfs/ ${ p #/ } " ] ] && __out += ( " ${ p #/ } " )
done < <( hb_default_profile_paths)
fi
}
# ==========================================================
# PBS CONFIG — auto-detect from storage.cfg + manual
# ==========================================================
hb_collect_pbs_configs( ) {
HB_PBS_NAMES = ( )
HB_PBS_REPOS = ( )
HB_PBS_SECRETS = ( )
HB_PBS_SOURCES = ( )
if [ [ -f /etc/pve/storage.cfg ] ] ; then
local current = "" server = "" datastore = "" username = "" pw_file pw_val
while IFS = read -r line; do
line = " ${ line %%#* } "
line = " ${ line # " ${ line %%[![ : space : ]]* } " } "
line = " ${ line % " ${ line ##*[![ : space : ]] } " } "
[ [ -z " $line " ] ] && continue
if [ [ $line = ~ ^pbs:[ [ :space:] ] *( .+) $ ] ] ; then
if [ [ -n " $current " && -n " $server " && -n " $datastore " && -n " $username " ] ] ; then
pw_file = " /etc/pve/priv/storage/ ${ current } .pw "
pw_val = " $( [ [ -f " $pw_file " ] ] && cat " $pw_file " || echo "" ) "
HB_PBS_NAMES += ( " $current " )
HB_PBS_REPOS += ( " ${ username } @ ${ server } : ${ datastore } " )
HB_PBS_SECRETS += ( " $pw_val " )
HB_PBS_SOURCES += ( "proxmox" )
fi
current = " ${ BASH_REMATCH [1] } " ; server = "" datastore = "" username = ""
elif [ [ -n " $current " ] ] ; then
[ [ $line = ~ ^[ [ :space:] ] +server[ [ :space:] ] +( .+) $ ] ] && server = " ${ BASH_REMATCH [1] } "
[ [ $line = ~ ^[ [ :space:] ] +datastore[ [ :space:] ] +( .+) $ ] ] && datastore = " ${ BASH_REMATCH [1] } "
[ [ $line = ~ ^[ [ :space:] ] +username[ [ :space:] ] +( .+) $ ] ] && username = " ${ BASH_REMATCH [1] } "
if [ [ $line = ~ ^[ a-zA-Z] +:[ [ :space:] ] &&
-n " $server " && -n " $datastore " && -n " $username " ] ] ; then
pw_file = " /etc/pve/priv/storage/ ${ current } .pw "
pw_val = " $( [ [ -f " $pw_file " ] ] && cat " $pw_file " || echo "" ) "
HB_PBS_NAMES += ( " $current " )
HB_PBS_REPOS += ( " ${ username } @ ${ server } : ${ datastore } " )
HB_PBS_SECRETS += ( " $pw_val " )
HB_PBS_SOURCES += ( "proxmox" )
current = "" server = "" datastore = "" username = ""
fi
fi
done < /etc/pve/storage.cfg
# Last stanza
if [ [ -n " $current " && -n " $server " && -n " $datastore " && -n " $username " ] ] ; then
pw_file = " /etc/pve/priv/storage/ ${ current } .pw "
pw_val = " $( [ [ -f " $pw_file " ] ] && cat " $pw_file " || echo "" ) "
HB_PBS_NAMES += ( " $current " )
HB_PBS_REPOS += ( " ${ username } @ ${ server } : ${ datastore } " )
HB_PBS_SECRETS += ( " $pw_val " )
HB_PBS_SOURCES += ( "proxmox" )
fi
fi
# Manual configs
local manual_cfg = " $HB_STATE_DIR /pbs-manual-configs.txt "
if [ [ -f " $manual_cfg " ] ] ; then
local line name repo sf
while IFS = read -r line; do
line = " ${ line %%#* } "
line = " ${ line # " ${ line %%[![ : space : ]]* } " } "
line = " ${ line % " ${ line ##*[![ : space : ]] } " } "
[ [ -z " $line " ] ] && continue
name = " ${ line %%|* } " ; repo = " ${ line ##*| } "
sf = " $HB_STATE_DIR /pbs-pass- ${ name } .txt "
HB_PBS_NAMES += ( " $name " ) ; HB_PBS_REPOS += ( " $repo " )
HB_PBS_SECRETS += ( " $( [ [ -f " $sf " ] ] && cat " $sf " || echo "" ) " )
HB_PBS_SOURCES += ( "manual" )
done < " $manual_cfg "
fi
}
hb_configure_pbs_manual( ) {
local name user host datastore repo secret
name = $( dialog --backtitle "ProxMenux" --title " $( hb_translate "Add PBS" ) " \
--inputbox " $( hb_translate "Configuration name:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " " PBS- $( date +%m%d) " 3>& 1 1>& 2 2>& 3) || return 1
[ [ -z " $name " ] ] && return 1
user = $( dialog --backtitle "ProxMenux" --title " $( hb_translate "Add PBS" ) " \
--inputbox " $( hb_translate "Username (e.g. root@pam or user@pbs!token):" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "root@pam" 3>& 1 1>& 2 2>& 3) || return 1
host = $( dialog --backtitle "ProxMenux" --title " $( hb_translate "Add PBS" ) " \
--inputbox " $( hb_translate "PBS host or IP address:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "" 3>& 1 1>& 2 2>& 3) || return 1
[ [ -z " $host " ] ] && return 1
datastore = $( dialog --backtitle "ProxMenux" --title " $( hb_translate "Add PBS" ) " \
--inputbox " $( hb_translate "Datastore name:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "" 3>& 1 1>& 2 2>& 3) || return 1
[ [ -z " $datastore " ] ] && return 1
secret = $( dialog --backtitle "ProxMenux" --title " $( hb_translate "Add PBS" ) " \
--insecure --passwordbox " $( hb_translate "Password or API token secret:" ) " \
" $HB_UI_PASS_H " " $HB_UI_PASS_W " "" 3>& 1 1>& 2 2>& 3) || return 1
repo = " ${ user } @ ${ host } : ${ datastore } "
mkdir -p " $HB_STATE_DIR "
local cfg_line = " ${ name } | ${ repo } "
local manual_cfg = " $HB_STATE_DIR /pbs-manual-configs.txt "
touch " $manual_cfg "
grep -Fxq " $cfg_line " " $manual_cfg " || echo " $cfg_line " >> " $manual_cfg "
printf '%s' " $secret " > " $HB_STATE_DIR /pbs-pass- ${ name } .txt "
chmod 600 " $HB_STATE_DIR /pbs-pass- ${ name } .txt "
HB_PBS_NAME = " $name " ; HB_PBS_REPOSITORY = " $repo " ; HB_PBS_SECRET = " $secret "
}
hb_select_pbs_repository( ) {
hb_collect_pbs_configs
local menu = ( ) i = 1 idx
for idx in " ${ !HB_PBS_NAMES[@] } " ; do
local src = " ${ HB_PBS_SOURCES [ $idx ] } "
local label = " ${ HB_PBS_NAMES [ $idx ] } — ${ HB_PBS_REPOS [ $idx ] } [ $src ] "
[ [ -z " ${ HB_PBS_SECRETS [ $idx ] } " ] ] && label += " ⚠ $( hb_translate "no password" ) "
menu += ( " $i " " $label " ) ; ( ( i++) )
done
menu += ( " $i " " $( hb_translate "+ Add new PBS manually" ) " )
local choice
choice = $( dialog --backtitle "ProxMenux" \
--title " $( hb_translate "Select PBS repository" ) " \
--menu " \n $( hb_translate "Available PBS repositories:" ) " \
" $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " " ${ menu [@] } " 3>& 1 1>& 2 2>& 3) || return 1
if [ [ " $choice " = = " $i " ] ] ; then
hb_configure_pbs_manual || return 1
else
local sel = $(( choice-1))
HB_PBS_NAME = " ${ HB_PBS_NAMES [ $sel ] } "
export HB_PBS_REPOSITORY = " ${ HB_PBS_REPOS [ $sel ] } "
HB_PBS_SECRET = " ${ HB_PBS_SECRETS [ $sel ] } "
if [ [ -z " $HB_PBS_SECRET " ] ] ; then
HB_PBS_SECRET = $( dialog --backtitle "ProxMenux" --title "PBS" \
--insecure --passwordbox \
" $( hb_translate "Password for:" ) $HB_PBS_NAME " \
" $HB_UI_PASS_H " " $HB_UI_PASS_W " "" 3>& 1 1>& 2 2>& 3) || return 1
mkdir -p " $HB_STATE_DIR "
printf '%s' " $HB_PBS_SECRET " > " $HB_STATE_DIR /pbs-pass- ${ HB_PBS_NAME } .txt "
chmod 600 " $HB_STATE_DIR /pbs-pass- ${ HB_PBS_NAME } .txt "
fi
fi
}
hb_ask_pbs_encryption( ) {
local key_file = " $HB_STATE_DIR /pbs-key.conf "
local enc_pass_file = " $HB_STATE_DIR /pbs-encryption-pass.txt "
export HB_PBS_KEYFILE_OPT = ""
export HB_PBS_ENC_PASS = ""
dialog --backtitle "ProxMenux" --title " $( hb_translate "Encryption" ) " \
--yesno " $( hb_translate "Encrypt this backup with a keyfile?" ) " \
" $HB_UI_YESNO_H " " $HB_UI_YESNO_W " || return 0
if [ [ -f " $key_file " ] ] ; then
export HB_PBS_KEYFILE_OPT = " --keyfile $key_file "
if [ [ -f " $enc_pass_file " ] ] ; then
HB_PBS_ENC_PASS = " $( <" $enc_pass_file " ) "
export HB_PBS_ENC_PASS
fi
msg_ok " $( hb_translate "Using existing encryption key:" ) $key_file "
return 0
fi
# No key — offer to create one
dialog --backtitle "ProxMenux" --title " $( hb_translate "Encryption" ) " \
--yesno " $( hb_translate "No encryption key found. Create one now?" ) " \
" $HB_UI_YESNO_H " " $HB_UI_YESNO_W " || return 0
local pass1 pass2
while true; do
pass1 = $( dialog --backtitle "ProxMenux" --insecure --passwordbox \
" $( hb_translate "Encryption passphrase (separate from PBS password):" ) " \
" $HB_UI_PASS_H " " $HB_UI_PASS_W " "" 3>& 1 1>& 2 2>& 3) || return 0
pass2 = $( dialog --backtitle "ProxMenux" --insecure --passwordbox \
" $( hb_translate "Confirm encryption passphrase:" ) " \
" $HB_UI_PASS_H " " $HB_UI_PASS_W " "" 3>& 1 1>& 2 2>& 3) || return 0
[ [ " $pass1 " = = " $pass2 " ] ] && break
dialog --backtitle "ProxMenux" \
--msgbox " $( hb_translate "Passphrases do not match. Try again." ) " 8 50
done
msg_info " $( hb_translate "Creating PBS encryption key..." ) "
if PBS_ENCRYPTION_PASSWORD = " $pass1 " \
proxmox-backup-client key create " $key_file " >/dev/null 2>& 1; then
printf '%s' " $pass1 " > " $enc_pass_file "
chmod 600 " $enc_pass_file "
msg_ok " $( hb_translate "Encryption key created:" ) $key_file "
HB_PBS_KEYFILE_OPT = " --keyfile $key_file "
HB_PBS_ENC_PASS = " $pass1 "
local key_warn_msg
key_warn_msg = " $( hb_translate "IMPORTANT: Back up this key file. Without it the backup cannot be restored." ) " $'\n\n' " $( hb_translate "Key:" ) $key_file "
dialog --backtitle "ProxMenux" --msgbox \
" $key_warn_msg " \
10 74
else
msg_error " $( hb_translate "Failed to create encryption key. Backup will proceed without encryption." ) "
fi
}
# ==========================================================
# BORG
# ==========================================================
hb_ensure_borg( ) {
command -v borg >/dev/null 2>& 1 && { echo "borg" ; return 0; }
local appimage = " $HB_STATE_DIR /borg "
local tmp_file
[ [ -x " $appimage " ] ] && { echo " $appimage " ; return 0; }
command -v sha256sum >/dev/null 2>& 1 || {
msg_error " $( hb_translate "sha256sum not found. Cannot verify Borg binary." ) "
return 1
}
msg_info " $( hb_translate "Borg not found. Downloading borg" ) ${ HB_BORG_VERSION } ... "
mkdir -p " $HB_STATE_DIR "
tmp_file = $( mktemp " $HB_STATE_DIR /.borg-download.XXXXXX " ) || return 1
if wget -qO " $tmp_file " " $HB_BORG_LINUX64_URL " ; then
if echo " ${ HB_BORG_LINUX64_SHA256 } $tmp_file " | sha256sum -c - >/dev/null 2>& 1; then
mv -f " $tmp_file " " $appimage "
else
rm -f " $tmp_file "
msg_error " $( hb_translate "Borg binary checksum verification failed." ) "
return 1
fi
chmod +x " $appimage "
msg_ok " $( hb_translate "Borg ready." ) "
echo " $appimage " ; return 0
fi
rm -f " $tmp_file "
msg_error " $( hb_translate "Failed to download Borg." ) "
return 1
}
hb_borg_init_if_needed( ) {
local borg_bin = " $1 " repo = " $2 " encrypt_mode = " $3 "
" $borg_bin " list " $repo " >/dev/null 2>& 1 && return 0
if " $borg_bin " help repo-create >/dev/null 2>& 1; then
" $borg_bin " repo-create -e " $encrypt_mode " " $repo "
else
" $borg_bin " init --encryption= " $encrypt_mode " " $repo "
fi
}
hb_prepare_borg_passphrase( ) {
local pass_file = " $HB_STATE_DIR /borg-pass.txt "
BORG_ENCRYPT_MODE = "none"
unset BORG_PASSPHRASE
if [ [ -f " $pass_file " ] ] ; then
export BORG_PASSPHRASE
BORG_PASSPHRASE = " $( <" $pass_file " ) "
BORG_ENCRYPT_MODE = "repokey"
return 0
fi
dialog --backtitle "ProxMenux" --title " $( hb_translate "Borg encryption" ) " \
--yesno " $( hb_translate "Encrypt this Borg repository?" ) " \
" $HB_UI_YESNO_H " " $HB_UI_YESNO_W " || return 0
local pass1 pass2
while true; do
pass1 = $( dialog --backtitle "ProxMenux" --insecure --passwordbox \
" $( hb_translate "Borg passphrase:" ) " \
" $HB_UI_PASS_H " " $HB_UI_PASS_W " "" 3>& 1 1>& 2 2>& 3) || return 1
pass2 = $( dialog --backtitle "ProxMenux" --insecure --passwordbox \
" $( hb_translate "Confirm Borg passphrase:" ) " \
" $HB_UI_PASS_H " " $HB_UI_PASS_W " "" 3>& 1 1>& 2 2>& 3) || return 1
[ [ " $pass1 " = = " $pass2 " ] ] && break
dialog --backtitle "ProxMenux" \
--msgbox " $( hb_translate "Passphrases do not match." ) " 8 50
done
mkdir -p " $HB_STATE_DIR "
printf '%s' " $pass1 " > " $pass_file "
chmod 600 " $pass_file "
export BORG_PASSPHRASE = " $pass1 "
export BORG_ENCRYPT_MODE = "repokey"
}
hb_select_borg_repo( ) {
local _borg_repo_var = " $1 "
local -n _borg_repo_ref = " $_borg_repo_var "
local type
type = $( dialog --backtitle "ProxMenux" \
--title " $( hb_translate "Borg repository location" ) " \
--menu " \n $( hb_translate "Select repository destination:" ) " \
" $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " \
"local" " $( hb_translate 'Local directory' ) " \
"usb" " $( hb_translate 'Mounted external disk' ) " \
"remote" " $( hb_translate 'Remote server via SSH' ) " \
3>& 1 1>& 2 2>& 3) || return 1
unset BORG_RSH
case " $type " in
local )
_borg_repo_ref = $( dialog --backtitle "ProxMenux" \
--inputbox " $( hb_translate "Borg repository path:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "/backup/borgbackup" \
3>& 1 1>& 2 2>& 3) || return 1
mkdir -p " $_borg_repo_ref " 2>/dev/null || true
; ;
usb)
local mnt
mnt = $( hb_prompt_mounted_path "/mnt/backup" ) || return 1
_borg_repo_ref = " $mnt /borgbackup "
mkdir -p " $_borg_repo_ref " 2>/dev/null || true
; ;
remote)
local user host rpath ssh_key
user = $( dialog --backtitle "ProxMenux" --inputbox " $( hb_translate "SSH user:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "root" 3>& 1 1>& 2 2>& 3) || return 1
host = $( dialog --backtitle "ProxMenux" --inputbox " $( hb_translate "SSH host or IP:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "" 3>& 1 1>& 2 2>& 3) || return 1
rpath = $( dialog --backtitle "ProxMenux" \
--inputbox " $( hb_translate "Remote repository path:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "/backup/borgbackup" \
3>& 1 1>& 2 2>& 3) || return 1
if dialog --backtitle "ProxMenux" \
--yesno " $( hb_translate "Use a custom SSH key?" ) " \
" $HB_UI_YESNO_H " " $HB_UI_YESNO_W " ; then
ssh_key = $( dialog --backtitle "ProxMenux" \
--fselect " $HOME /.ssh/ " 12 70 3>& 1 1>& 2 2>& 3) || return 1
export BORG_RSH = " ssh -i $ssh_key -o StrictHostKeyChecking=accept-new "
fi
_borg_repo_ref = " ssh:// $user @ $host / $rpath "
; ;
esac
}
# ==========================================================
# COMMON PROMPTS
# ==========================================================
hb_trim_dialog_value( ) {
local value = " $1 "
value = " ${ value // $'\r' / } "
value = " ${ value // $'\n' / } "
value = " ${ value # " ${ value %%[![ : space : ]]* } " } "
value = " ${ value % " ${ value ##*[![ : space : ]] } " } "
printf '%s' " $value "
}
hb_prompt_mounted_path( ) {
local default_path = " ${ 1 :- /mnt/backup } "
local out
out = $( dialog --backtitle "ProxMenux" \
--title " $( hb_translate "Mounted disk path" ) " \
--inputbox " $( hb_translate "Path where the external disk is mounted:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " " $default_path " 3>& 1 1>& 2 2>& 3) || return 1
out = $( hb_trim_dialog_value " $out " )
[ [ -n " $out " && -d " $out " ] ] || { msg_error " $( hb_translate "Path does not exist." ) " ; return 1; }
if ! mountpoint -q " $out " 2>/dev/null; then
dialog --backtitle "ProxMenux" --title " $( hb_translate "Warning" ) " \
--yesno " $( hb_translate "This path is not a registered mount point. Use it anyway?" ) " \
" $HB_UI_YESNO_H " " $HB_UI_YESNO_W " || return 1
fi
echo " $out "
}
hb_prompt_dest_dir( ) {
local selection out
selection = $( dialog --backtitle "ProxMenux" \
--title " $( hb_translate "Select destination" ) " \
--menu " \n $( hb_translate "Choose where to save the backup:" ) " \
" $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " \
"vzdump" " $( hb_translate '/var/lib/vz/dump (Proxmox default vzdump path)' ) " \
"backup" " $( hb_translate '/backup' ) " \
"local" " $( hb_translate 'Custom local directory' ) " \
"usb" " $( hb_translate 'Mounted external disk' ) " \
3>& 1 1>& 2 2>& 3) || return 1
case " $selection " in
vzdump) out = "/var/lib/vz/dump" ; ;
backup) out = "/backup" ; ;
local )
out = $( dialog --backtitle "ProxMenux" \
--inputbox " $( hb_translate "Enter directory path:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "/backup" 3>& 1 1>& 2 2>& 3) || return 1
; ;
usb) out = $( hb_prompt_mounted_path "/mnt/backup" ) || return 1 ; ;
esac
out = $( hb_trim_dialog_value " $out " )
[ [ -n " $out " ] ] || return 1
mkdir -p " $out " || { msg_error " $( hb_translate "Cannot create:" ) $out " ; return 1; }
echo " $out "
}
hb_prompt_restore_source_dir( ) {
local choice out
choice = $( dialog --backtitle "ProxMenux" \
--title " $( hb_translate "Restore source location" ) " \
--menu " \n $( hb_translate "Where are the backup archives stored?" ) " \
" $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " \
"vzdump" " $( hb_translate '/var/lib/vz/dump (Proxmox default)' ) " \
"backup" " $( hb_translate '/backup' ) " \
"usb" " $( hb_translate 'Mounted external disk' ) " \
"custom" " $( hb_translate 'Custom path' ) " \
3>& 1 1>& 2 2>& 3) || return 1
case " $choice " in
vzdump) out = "/var/lib/vz/dump" ; ;
backup) out = "/backup" ; ;
usb) out = $( hb_prompt_mounted_path "/mnt/backup" ) || return 1 ; ;
custom)
out = $( dialog --backtitle "ProxMenux" \
--inputbox " $( hb_translate "Enter path:" ) " \
" $HB_UI_INPUT_H " " $HB_UI_INPUT_W " "/backup" 3>& 1 1>& 2 2>& 3) || return 1
; ;
esac
out = $( hb_trim_dialog_value " $out " )
[ [ -n " $out " && -d " $out " ] ] || {
msg_error " $( hb_translate "Directory does not exist." ) "
return 1
}
echo " $out "
}
hb_prompt_local_archive( ) {
local base_dir = " $1 "
local title = " ${ 2 :- $( hb_translate "Select backup archive" ) } "
local -a rows = ( ) files = ( ) menu = ( )
# Single find pass using -printf: no per-file stat subprocesses.
# maxdepth 6 catches nested backup layouts commonly used in /var/lib/vz/dump.
mapfile -t rows < <(
find " $base_dir " -maxdepth 6 -type f \
\( -name '*.tar.zst' -o -name '*.tar.gz' -o -name '*.tar' \) \
-printf '%T@|%s|%p\n' 2>/dev/null \
| sort -t'|' -k1,1nr \
| head -200
)
if [ [ ${# rows [@] } -eq 0 ] ] ; then
local no_backups_msg
no_backups_msg = " $( hb_translate "No backup archives were found in:" ) $base_dir " $'\n\n' " $( hb_translate "Select another source path and try again." ) "
dialog --backtitle "ProxMenux" \
--title " $( hb_translate "No backups found" ) " \
--msgbox " $no_backups_msg " \
10 78 || true
return 1
fi
local i = 1 row epoch size path date_str size_str label
for row in " ${ rows [@] } " ; do
epoch = " ${ row %%|* } " ; row = " ${ row #*| } "
size = " ${ row %%|* } " ; path = " ${ row #*| } "
epoch = " ${ epoch %%.* } " # drop sub-second fraction from %T@
date_str = $( date -d " @ $epoch " '+%Y-%m-%d %H:%M' 2>/dev/null || echo "-" )
size_str = $( numfmt --to= iec-i --suffix= B " $size " 2>/dev/null || echo " ${ size } B " )
label = " ${ path # $base_dir / } $date_str $size_str "
files += ( " $path " ) ; menu += ( " $i " " $label " ) ; ( ( i++) )
done
local choice
choice = $( dialog --backtitle "ProxMenux" --title " $title " \
--menu " \n $( hb_translate "Detected backups — newest first:" ) " \
" $HB_UI_MENU_H " " $HB_UI_MENU_W " " $HB_UI_MENU_LIST " " ${ menu [@] } " 3>& 1 1>& 2 2>& 3) || return 1
echo " ${ files [ $(( choice-1)) ] } "
}
# ==========================================================
# UTILITIES
# ==========================================================
hb_human_elapsed( ) {
local secs = " $1 "
if ( ( secs < 60 ) ) ; then printf '%ds' " $secs "
elif ( ( secs < 3600 ) ) ; then printf '%dm %ds' " $(( secs/60)) " " $(( secs%60)) "
else printf '%dh %dm' " $(( secs/3600)) " " $(( ( secs%3600) / 60 )) "
fi
}
hb_file_size( ) {
local path = " $1 "
if [ [ -f " $path " ] ] ; then
numfmt --to= iec-i --suffix= B " $( stat -c %s " $path " 2>/dev/null || echo 0) " 2>/dev/null \
|| du -sh " $path " 2>/dev/null | awk '{print $1}'
elif [ [ -d " $path " ] ] ; then
du -sh " $path " 2>/dev/null | awk '{print $1}'
else
echo "-"
fi
}
hb_show_log( ) {
local logfile = " $1 " title = " ${ 2 :- $( hb_translate "Operation log" ) } "
[ [ -f " $logfile " && -s " $logfile " ] ] || return 0
dialog --backtitle "ProxMenux" --exit-label "OK" \
--title " $title " --textbox " $logfile " 26 110 || true
}
hb_require_cmd( ) {
local cmd = " $1 " pkg = " ${ 2 :- $1 } "
command -v " $cmd " >/dev/null 2>& 1 && return 0
if command -v apt-get >/dev/null 2>& 1; then
msg_warn " $( hb_translate "Installing dependency:" ) $pkg "
apt-get update -qq >/dev/null 2>& 1 && apt-get install -y " $pkg " >/dev/null 2>& 1
fi
command -v " $cmd " >/dev/null 2>& 1
}