#!/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) source ./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" ) # ---------------------------------------------------------------------------- # Default values and constants # ---------------------------------------------------------------------------- SYSTEM_EXCLUDES=( "/dev/*" "/proc/*" "/sys/*" "/tmp/*" "/run/*" "/mnt/*" "/media/*" "swapfile" "lost+found" ".cache" "Downloads" ".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 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 # ---------------------------------------------------------------------------- # 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 # ---------------------------------------------------------------------------- # Build rsync command # ---------------------------------------------------------------------------- CMD=("rsync" "-a" "--delete") if [[ "$NTFS" == false ]]; then CMD+=("-A" "-X") fi [[ "$SYSTEM" == true ]] && CMD+=("-H") CMD+=("-P") 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 "┌──────────────────────────────────────────┐" echo "│ Executing Rsync Process │" echo "└──────────────────────────────────────────┘" echo "Command : ${CMD[*]}" echo # Uncomment to execute "${CMD[@]}" echo echo "┌──────────────────────────────────────────┐" echo "│ Backup Process Completed │" echo "└──────────────────────────────────────────┘"