99085adc76
- Flask backend with 23 API routes (entertainment, telegram, docker, system) - Alpine.js + Tailwind CSS dark-mode SPA with 4 tabs - pos-dashboard CLI tool with port/config management - Mobile slide-in drawer with swipe-to-close - Sticky header stays pinned on scroll - Tab completion fixes for category-less tools - POST /api/telegram/commands endpoint for adding commands
226 lines
8.3 KiB
Bash
Executable File
226 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
set -E
|
|
# POS: system backup — Encrypted (AES-256) folder snapshots (tar + gpg)
|
|
# POS_FLAGS: --service --no-encrypt
|
|
# POS_CONFIG: notify | notify.env | NOTIFY_PLATFORM=:Comma-separated notify platforms (default telegram) — shared by backup, firewall, share nfs client/server
|
|
|
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
|
source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh"
|
|
source "$(dirname "$0")/../lib/usb-lib.sh" 2>/dev/null || source "$(dirname "$0")/usb-lib.sh"
|
|
|
|
load_system_env
|
|
EFF_ROOTS="${BACKUP_SERVICE_ROOTS:-/srv $HOME/srv}"
|
|
|
|
trap 'notify_send "Backup FAILED: ${FOLDER:-unknown}"' ERR
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: pos system backup <folder-path>
|
|
pos system backup <folder-path> --no-encrypt
|
|
pos system backup --service
|
|
|
|
Create a gpg-encrypted (AES-256) tar.gz snapshot of a folder and verify it.
|
|
The archive password is prompted twice and never stored. With --no-encrypt
|
|
(or BACKUP_ENCRYPT=0) the backup is kept as a plain .tar.gz — no password,
|
|
headless/cron safe.
|
|
|
|
Modes:
|
|
<folder-path> Back up that folder directly.
|
|
--no-encrypt Skip encryption (no password prompt, artifact stays .tar.gz).
|
|
--service List folders under /srv and ~/srv, pick one, back it up.
|
|
|
|
The final artifact <name>_<date>.tar.gz[.gpg] is written to the current directory.
|
|
After it verifies, connected USB storage is offered: the copy lands in
|
|
<usb>/backups/ and is sha256-verified 100% before it is announced. A stick
|
|
that is plugged in but not mounted is offered a mount first (sudo, mirrors
|
|
the usb-automount scheme) before the copy.
|
|
|
|
Environment:
|
|
BACKUP_ENCRYPT Set to 0 to skip encryption (same as --no-encrypt)
|
|
(default: 1)
|
|
BACKUP_SERVICE_ROOTS Space-separated roots for --service
|
|
(effective: ${EFF_ROOTS})
|
|
BACKUP_USB_ROOT USB root to copy finished backups to
|
|
(default: auto-detect mounted USB storage)
|
|
BACKUP_MOUNT_BASE Where to mount an unmounted USB stick offered
|
|
during the copy (default: /media; alias of
|
|
USB_MOUNT_BASE)
|
|
BACKUP_USB_BYID by-id dir used to corroborate USB detection
|
|
(default: /dev/disk/by-id; alias of USB_BYID)
|
|
(loaded from ~/.config/linux_post_install/system.env unless exported)
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
command -v tar &>/dev/null || err "tar not found — install with: sudo apt install tar"
|
|
|
|
# ── USB copy (optional post-backup step) ─────────────────────────
|
|
# Detection runs AFTER the backup finished, so a stick plugged in while
|
|
# the archive was being made is found. The copy lands in <usb>/backups/
|
|
# and the transfer is proven 100% (sha256 source vs copy) before any
|
|
# success is announced. BACKUP_USB_ROOT pins the root and skips
|
|
# detection; otherwise usb_pick_root (lib/usb-lib.sh) auto-detects,
|
|
# offers to mount unmounted sticks, and picks the target.
|
|
usb_copy_offer() {
|
|
local archive="$1" root="" dest_dir="" dest="" src_sum="" dst_sum=""
|
|
|
|
command -v lsblk &>/dev/null || { warn "lsblk not found — USB copy skipped"; return 0; }
|
|
command -v jq &>/dev/null || { warn "jq not found — USB copy skipped"; return 0; }
|
|
|
|
section "USB copy"
|
|
|
|
if [ -n "${BACKUP_USB_ROOT:-}" ]; then
|
|
root="$BACKUP_USB_ROOT"
|
|
if ! confirm "Copy backup to ${root%/}/backups/?" n; then
|
|
log "Skipped — backup stays local: $archive"
|
|
return 0
|
|
fi
|
|
else
|
|
usb_pick_root "Copy backup to" "backups" "backup stays local: $archive" || {
|
|
log "Skipped — backup stays local: $archive"
|
|
return 0
|
|
}
|
|
root="$USB_ROOT"
|
|
fi
|
|
|
|
dest_dir="${root%/}/backups"
|
|
dest="$dest_dir/$(basename "$archive")"
|
|
|
|
mkdir -p "$dest_dir"
|
|
log "Copying to $dest ..."
|
|
cp "$archive" "$dest_dir/"
|
|
chmod 600 "$dest" 2>/dev/null \
|
|
|| warn "Could not chmod 600 the USB copy (vfat filesystem?)"
|
|
|
|
log "Verifying transfer (sha256)..."
|
|
src_sum="$(sha256sum "$archive" | cut -d' ' -f1)"
|
|
dst_sum="$(sha256sum "$dest" | cut -d' ' -f1)"
|
|
if [ "$src_sum" != "$dst_sum" ]; then
|
|
warn "USB copy FAILED verification — checksum mismatch:"
|
|
warn " source: $src_sum $archive"
|
|
warn " copy : $dst_sum $dest"
|
|
notify_send "USB copy FAILED for $archive — checksum mismatch on $dest"
|
|
exit 1
|
|
fi
|
|
ok "Transfer verified 100% (sha256 match): $dest"
|
|
notify_send "Backup copied to USB: $dest (sha256 verified)"
|
|
}
|
|
|
|
# ── USB detection / mount offer / pick flow ─────────────────────
|
|
# Shared with pos-media-sync: lib/usb-lib.sh (usb_detect /
|
|
# usb_related_present / usb_mount_offer / usb_pick_root).
|
|
SERVICE=0
|
|
ENCRYPT=1
|
|
[ "${BACKUP_ENCRYPT:-1}" = "0" ] && ENCRYPT=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-h|--help) usage ;;
|
|
--service) SERVICE=1 ;;
|
|
--no-encrypt) ENCRYPT=0 ;;
|
|
*) FOLDER="$arg" ;;
|
|
esac
|
|
done
|
|
{ [ "$SERVICE" -eq 1 ] || [ -n "${FOLDER:-}" ]; } || err "Missing folder path (or use --service)"
|
|
|
|
if [ "$SERVICE" -eq 1 ]; then
|
|
if [ -n "${BACKUP_SERVICE_ROOTS:-}" ]; then
|
|
read -r -a roots <<< "$BACKUP_SERVICE_ROOTS"
|
|
else
|
|
roots=(/srv "$HOME/srv")
|
|
fi
|
|
|
|
idx=0
|
|
for root in "${roots[@]}"; do
|
|
[ -d "$root" ] || { warn "Root not found: $root"; continue; }
|
|
|
|
dirs=()
|
|
while IFS= read -r d; do
|
|
dirs+=("$d")
|
|
done < <(find "$root" -maxdepth 1 -mindepth 1 -type d | sort)
|
|
|
|
[ ${#dirs[@]} -gt 0 ] || { warn "No folders in $root"; continue; }
|
|
|
|
echo
|
|
echo "${root}:"
|
|
for d in "${dirs[@]}"; do
|
|
idx=$((idx + 1))
|
|
names[$idx]="$d"
|
|
printf "%2d) %s\n" "$idx" "$d"
|
|
done
|
|
done
|
|
|
|
[ "$idx" -gt 0 ] || err "No folders found under: ${roots[*]}"
|
|
|
|
read -rp "Select folder number: " choice
|
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > idx )); then
|
|
err "Invalid selection: $choice"
|
|
fi
|
|
FOLDER="${names[$choice]}"
|
|
fi
|
|
|
|
[ -d "$FOLDER" ] || err "Folder not found: $FOLDER"
|
|
|
|
NAME="$(basename "$FOLDER")"
|
|
DATE="$(date +%Y-%m-%d_%H-%M-%S)"
|
|
ARCHIVE="${NAME}_${DATE}.tar.gz"
|
|
|
|
# ── Backup steps ────────────────────────────────────────────────
|
|
# Total steps: 3 with encryption, 2 without
|
|
_total=2
|
|
[ "$ENCRYPT" -eq 1 ] && _total=3
|
|
|
|
step 1 "$_total" "Creating backup archive"
|
|
echo "Source : $FOLDER"
|
|
echo "Output : $ARCHIVE"
|
|
|
|
sudo tar -czvf "$ARCHIVE" -C "$(dirname "$FOLDER")" "$NAME"
|
|
|
|
step 2 "$_total" "Verifying archive"
|
|
tar -tzf "$ARCHIVE" > /dev/null
|
|
log "Archive verified"
|
|
|
|
if [ "$ENCRYPT" -eq 1 ]; then
|
|
command -v gpg &>/dev/null || err "gpg not found — install with: sudo apt install gnupg"
|
|
|
|
while true; do
|
|
read -s -rp "Enter backup password: " PASS
|
|
echo
|
|
read -s -rp "Confirm backup password: " CONFIRM
|
|
echo
|
|
if [ -n "$PASS" ] && [ "$PASS" = "$CONFIRM" ]; then
|
|
break
|
|
fi
|
|
warn "Passwords are empty or do not match — try again"
|
|
done
|
|
unset CONFIRM
|
|
|
|
step 3 "$_total" "Encrypting backup"
|
|
_passfd="$(mktemp)"; printf '%s' "$PASS" > "$_passfd"; chmod 600 "$_passfd"
|
|
gpg --batch --yes --passphrase-file "$_passfd" --symmetric --cipher-algo AES256 "$ARCHIVE" || { rm -f "$_passfd"; err "GPG encryption failed"; }
|
|
rm -f "$_passfd"
|
|
|
|
rm -f "$ARCHIVE"
|
|
ARCHIVE="${ARCHIVE}.gpg"
|
|
chmod 600 "$ARCHIVE"
|
|
|
|
log "Verifying encrypted backup..."
|
|
_passfd="$(mktemp)"; printf '%s' "$PASS" > "$_passfd"; chmod 600 "$_passfd"
|
|
gpg --batch --quiet --passphrase-file "$_passfd" --decrypt "$ARCHIVE" | tar -tzf - > /dev/null || { rm -f "$_passfd"; err "GPG verification failed"; }
|
|
rm -f "$_passfd"
|
|
|
|
unset PASS
|
|
else
|
|
chmod 600 "$ARCHIVE"
|
|
log "No encryption requested — keeping $ARCHIVE"
|
|
fi
|
|
echo
|
|
log "Backup completed: $ARCHIVE"
|
|
notify_send "Backup completed: $ARCHIVE"
|
|
|
|
# Optional: detect a USB stick connected after the backup finished, offer to
|
|
# copy the archive to <usb>/backups/, and prove the transfer 100%. From here
|
|
# on a failure is a USB-copy problem, not a backup problem.
|
|
trap 'notify_send "USB copy FAILED: ${ARCHIVE:-unknown}"' ERR
|
|
usb_copy_offer "$ARCHIVE"
|