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
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")
|
|
|
|
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 "└──────────────────────────────────────────┘"
|