mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-23 12:10:38 +00:00
771 lines
29 KiB
Bash
771 lines
29 KiB
Bash
#!/bin/bash
|
|
# ==========================================================
|
|
# ProxMenux - Host Config Backup/Restore - Shared Library
|
|
# ==========================================================
|
|
# Author : MacRimi
|
|
# Copyright : (c) 2024 MacRimi
|
|
# License : MIT
|
|
# 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
|
|
}
|