You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

269 lines
8.8 KiB

#!/bin/bash
# =============================================================================
# Backup Tool — Argument-Driven rsync Wrapper
# =============================================================================
# Description:
# A modular backup script using rsync with smart CLI parsing
# Supports: local/system/user backups, remote SSH push/pull, link-dest snapshots
# =============================================================================
# Load CLI parser (must be in same or relative path)
# # Resolve the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/cli/arg_parser.sh"
# Flag check utility
get_flag() {
local name="$1"
[[ -n "${FLAG_GROUPS[$name]}" && "${FLAG_GROUPS[$name]}" == "true" ]]
}
# ----------------------------------------------------------------------------
# Define valid flags
# ----------------------------------------------------------------------------
VALID_FLAGS=(
"source" "target" "dry-run" "verbose" "compress" "system"
"ssh-send" "ssh-receive" "link" "ntfs" "name" "exclude"
"progress" "sudo"
)
# ----------------------------------------------------------------------------
# Default values and constants
# ----------------------------------------------------------------------------
SYSTEM_EXCLUDES=(
"/dev"
"/proc"
"/sys"
"/tmp"
"/run"
"/mnt"
"/media"
"/swapfile"
"/lost+found"
"/home/*/.cache"
"/home/*/Downloads"
"/home/*/.ecryptfs"
)
# ----------------------------------------------------------------------------
# Parse CLI args
# ----------------------------------------------------------------------------
parse_grouped_args "$@"
# ----------------------------------------------------------------------------
# Validate flags
# ----------------------------------------------------------------------------
for key in "${!FLAG_GROUPS[@]}"; do
found=false
for valid in "${VALID_FLAGS[@]}"; do
[[ "$key" == "$valid" ]] && found=true && break
done
if [[ "$found" == false ]]; then
echo "Error: Unknown flag '--$key'"
exit 1
fi
done
# ----------------------------------------------------------------------------
# Extract values from flags
# ----------------------------------------------------------------------------
IFS=' ' read -r -a SOURCE <<<"${FLAG_GROUPS["source"]}"
IFS=' ' read -r -a TARGET <<<"${FLAG_GROUPS["target"]}"
IFS=' ' read -r -a EXCLUDES <<<"${FLAG_GROUPS["exclude"]}"
BACKUP_NAME=""
[[ -n "${FLAG_GROUPS["name"]}" ]] && BACKUP_NAME="${FLAG_GROUPS["name"]}"
DRY_RUN=false
VERBOSE=false
COMPRESS=false
SYSTEM=false
NTFS=false
PROGRESS=false
SUDO_PRIV=flase
get_flag "dry-run" && DRY_RUN=true
get_flag "verbose" && VERBOSE=true
get_flag "compress" && COMPRESS=true
get_flag "system" && SYSTEM=true
get_flag "ntfs" && NTFS=true
get_flag "progress" && PROGRESS=true
get_flag "sudo" && SUDO_PRIV=true
# ----------------------------------------------------------------------------
# Required argument checks
# ----------------------------------------------------------------------------
if [[ -z "${SOURCE[0]}" || -z "${TARGET[0]}" ]]; then
echo "Error: --source and --target are required."
exit 1
elif [[ ${#SOURCE[@]} -gt 1 || ${#TARGET[@]} -gt 1 ]]; then
echo "Error: --source and --target must be single paths only."
exit 1
fi
# ----------------------------------------------------------------------------
# Backup naming (target path suffix)
# ----------------------------------------------------------------------------
if [[ -n "$BACKUP_NAME" ]]; then
TARGET_PATH="${TARGET[0]%/}/$BACKUP_NAME"
else
TARGET_PATH="${TARGET[0]}"
fi
# ----------------------------------------------------------------------------
# SSH configuration (push or pull)
# ----------------------------------------------------------------------------
SSH_SEND="${FLAG_GROUPS["ssh-send"]}"
SSH_RECEIVE="${FLAG_GROUPS["ssh-receive"]}"
if [[ -n "$SSH_SEND" && -n "$SSH_RECEIVE" ]]; then
echo "Error: Only one of --ssh-send or --ssh-receive can be set."
exit 1
fi
if [[ -n "$SSH_SEND" && ! "$SSH_SEND" =~ ^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+$ ]]; then
echo "Error: --ssh-send must be in user@host format."
exit 1
fi
if [[ -n "$SSH_RECEIVE" && ! "$SSH_RECEIVE" =~ ^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+$ ]]; then
echo "Error: --ssh-receive must be in user@host format."
exit 1
fi
if [[ -n "$SSH_SEND" ]]; then
TARGET_PATH="$SSH_SEND:$TARGET_PATH"
elif [[ -n "$SSH_RECEIVE" ]]; then
SOURCE[0]="$SSH_RECEIVE:${SOURCE[0]}"
fi
# ----------------------------------------------------------------------------
# Link-dest configuration (incremental snapshots)
# ----------------------------------------------------------------------------
LINK_DIR=""
if [[ -n "${FLAG_GROUPS["link"]}" ]]; then
LINK_DIR="${FLAG_GROUPS["link"]}"
if [[ ! "$LINK_DIR" =~ ^/.* ]]; then
echo "Error: --link must be an absolute path."
exit 1
fi
fi
# ----------------------------------------------------------------------------
# System-specific excludes
# ----------------------------------------------------------------------------
if [[ "$SYSTEM" == true ]]; then
EXCLUDES+=("${SYSTEM_EXCLUDES[@]}")
fi
# -----------------------------------------------------------------------------
# Prevent recursive backup of target inside source and Link (Ask me how I Know)
# -----------------------------------------------------------------------------
if [[ "${SOURCE[0]}" == "/" || "${SOURCE[0]}" == /* ]]; then
# Resolve absolute target path
TARGET_REAL=$(realpath "$TARGET_PATH")
if [[ "$TARGET_REAL" == "${SOURCE[0]}"* ]]; then
EXCLUDES+=("$TARGET_REAL")
fi
fi
if [[ -n "$LINK_DIR" ]]; then
# Automatically exclude the link destination itself
EXCLUDES+=("$LINK_DIR")
fi
# ----------------------------------------------------------------------------
# Build rsync command
# ----------------------------------------------------------------------------
CMD=("rsync" "-a" "--delete" "--info=progress2")
if [[ "$NTFS" == false ]]; then
CMD+=("-A" "-X")
fi
[[ "$SYSTEM" == true ]] && CMD+=("-H")
[[ "$PROGRESS" == true ]] && CMD+=("-P")
[[ "$PROGRESS" == false ]] && CMD+=("--partial")
[[ "$SUDO_PRIV" == true ]] && CMD+=("--rsync-path=sudo rsync")
CMD+=("--delete-excluded")
$VERBOSE && CMD+=("-v")
$DRY_RUN && CMD+=("--dry-run")
$COMPRESS && CMD+=("-z")
[[ -n "$LINK_DIR" ]] && CMD+=("--link-dest=$LINK_DIR")
# Add excludes here
if [[ ${#EXCLUDES[@]} -gt 0 ]]; then
for ex in "${EXCLUDES[@]}"; do
CMD+=("--exclude=$ex")
done
fi
CMD+=("${SOURCE[0]}" "$TARGET_PATH")
# ----------------------------------------------------------------------------
# Summary Output
# ----------------------------------------------------------------------------
echo "┌──────────────────────────────────────────┐"
echo "│ Starting Backup Process │"
echo "└──────────────────────────────────────────┘"
echo
echo "Directories:"
echo " ├─ Source : ${SOURCE[0]}"
echo " └─ Target : $TARGET_PATH"
echo
echo "Mode:"
if [[ "$SYSTEM" == true ]]; then
echo " └─ System Backup : Default excludes applied"
else
echo " └─ User Backup : No system-specific excludes"
fi
echo
echo "Configuration:"
flag_printed=false
[[ "$DRY_RUN" == true ]] && echo " ├─ Dry-run : enabled" && flag_printed=true
[[ "$VERBOSE" == true ]] && echo "${flag_printed:+ ├─ }Verbose : enabled" && flag_printed=true
[[ "$COMPRESS" == true ]] && echo " └─ Compression : enabled"
[[ "$flag_printed" == false ]] && echo " └─ No Configuration set"
echo
echo "Snapshot:"
echo " ├─ Backup Name : ${BACKUP_NAME:-none}"
echo " └─ Link From : ${LINK_DIR:-none}"
echo
echo "Excludes:"
if [[ ${#EXCLUDES[@]} -gt 0 ]]; then
last_index=$((${#EXCLUDES[@]} - 1))
for i in "${!EXCLUDES[@]}"; do
prefix=$([[ "$i" -eq "$last_index" ]] && echo " └─" || echo " ├─")
echo "$prefix ${EXCLUDES[$i]}"
done
else
echo " └─ No Excludes Set"
fi
echo
echo "Command : ${CMD[*]}"
read -rp "If correct press neter to continue..."
echo
echo "┌──────────────────────────────────────────┐"
echo "│ Executing Rsync Process │"
echo "└──────────────────────────────────────────┘"
echo
# Uncomment to execute
"${CMD[@]}"
echo
echo "┌──────────────────────────────────────────┐"
echo "│ Backup Process Completed │"
echo "└──────────────────────────────────────────┘"