From d567a8e6042b88213aced5414ba07d84404de568 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:10:58 +0100 Subject: [PATCH 1/9] improve restore function --- backup_script.sh | 192 +++++++++++++++++++++++++++++------------------ 1 file changed, 117 insertions(+), 75 deletions(-) diff --git a/backup_script.sh b/backup_script.sh index 1cf56c8..ff17884 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -1,5 +1,5 @@ #!/bin/bash -# ===================== v0.32 - 2025.08.13 ======================== +# ===================== v0.33 - 2025.08.14 ======================== # # Example backup.conf: # BACKUP_DIRS="/home/user/test/./ /var/www/./" @@ -359,14 +359,19 @@ run_restore_mode() { if [[ "$dir_choice" == "$RECYCLE_OPTION" ]]; then printf "${C_BOLD}${C_CYAN}--- Browse Recycle Bin ---${C_RESET}\n" local remote_recycle_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" - local date_folders - date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true - if [[ -z "$date_folders" ]]; then - echo "❌ No dated folders found in the recycle bin. Nothing to restore." >&2 - return 1 + local date_folders; date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true + local valid_folders=() + for f in $date_folders; do + if [[ "$f" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$ ]]; then + valid_folders+=( "$f" ) + fi + done + date_folders=("${valid_folders[@]}") + if [[ ${#date_folders[@]} -eq 0 ]]; then + echo "❌ No validly-named backup folders found in the recycle bin." >&2; return 1 fi printf "${C_YELLOW}Select a backup run (date_time) to browse:${C_RESET}\n" - select date_choice in $date_folders "Cancel"; do + select date_choice in "${date_folders[@]}" "Cancel"; do if [[ "$date_choice" == "Cancel" ]]; then echo "Restore cancelled."; return 0; elif [[ -n "$date_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi @@ -374,61 +379,117 @@ run_restore_mode() { local remote_date_path="${remote_recycle_path}/${date_choice}" printf "${C_BOLD}--- Files available from ${date_choice} (showing first 20) ---${C_RESET}\n" local remote_listing_source="${BOX_ADDR}:${remote_date_path}/" - rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" . 2>/dev/null | head -n 20 || echo "No files found for this date." + rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" /dev/null | head -n 20 || echo "No files found for this date." printf "${C_BOLD}--------------------------------------------------------${C_RESET}\n" - printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}" - read -r specific_path + printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}"; read -r specific_path + if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then + echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 + fi specific_path=$(echo "$specific_path" | sed 's#^/##') if [[ -z "$specific_path" ]]; then echo "❌ Path cannot be empty. Aborting."; return 1; fi full_remote_source="${BOX_ADDR}:${remote_date_path}/${specific_path}" - if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" . >/dev/null 2>&1; then - echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2 - return 1 + if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" /dev/null >/dev/null 2>&1; then + echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2; return 1 fi - default_local_dest="/${specific_path}" - item_for_display="(from Recycle Bin) '${specific_path}'" + default_local_dest="/${specific_path}"; item_for_display="(from Recycle Bin) '${specific_path}'" elif [[ "$dir_choice" == "Cancel" ]]; then - echo "Restore cancelled." - return 0 + echo "Restore cancelled."; return 0 else item_for_display="the entire directory '${dir_choice}'" while true; do - printf "\n${C_YELLOW}Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET}" - read -r choice + printf "\n${C_YELLOW}Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET}"; read -r choice case "$choice" in - entire) - is_full_directory_restore=true - break - ;; + entire) is_full_directory_restore=true; break ;; specific) - local specific_path_prompt - printf -v specific_path_prompt "Enter the path relative to '%s' to restore: " "$dir_choice" - printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt" - read -er specific_path + printf -v specific_path_prompt "Enter the path relative to '%s' to restore: " "$dir_choice"; printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt"; read -er specific_path + if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then + echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 + fi specific_path=$(echo "$specific_path" | sed 's#^/##') if [[ -n "$specific_path" ]]; then - restore_path="$specific_path" - item_for_display="'$restore_path' from '${dir_choice}'" - break + restore_path="$specific_path"; item_for_display="'$restore_path' from '${dir_choice}'"; break else echo "Path cannot be empty. Please try again or choose 'entire'." - fi - ;; + fi ;; *) echo "Invalid choice. Please answer 'entire' or 'specific'." ;; esac done local relative_path="${dir_choice#*./}" - full_remote_source="${REMOTE_TARGET}${relative_path}${restore_path}" + local remote_base="${REMOTE_TARGET%/}" + full_remote_source="${remote_base}/${relative_path#/}" + if [[ -n "$restore_path" ]]; then + full_remote_source="${full_remote_source%/}/${restore_path#/}" + fi if [[ -n "$restore_path" ]]; then - default_local_dest=$(echo "${dir_choice}${restore_path}" | sed 's#/\./#/#') + default_local_dest=$(echo "${dir_choice}${restore_path}" | sed 's#/\./#/#g') else - default_local_dest=$(echo "$dir_choice" | sed 's#/\./#/#') + default_local_dest=$(echo "$dir_choice" | sed 's#/\./#/#g') fi fi - local final_dest - printf "\n${C_YELLOW}Enter the destination path.\n${C_DIM}Press [Enter] to use the original location (%s):${C_RESET} " "$default_local_dest" - read -r final_dest + local final_dest + printf "\n%s\n" "${C_BOLD}--------------------------------------------------------" + printf "%s\n" " Restore Destination" + printf "%s\n" "--------------------------------------------------------${C_RESET}" + printf "%s\n\n" "Enter the absolute destination path for the restore." + printf "%s\n" "${C_YELLOW}Default (original location):${C_RESET}" + printf "${C_CYAN}%s${C_RESET}\n\n" "$default_local_dest" + printf "%s\n" "Press [Enter] to use the default path, or enter a new one." + read -rp "> " final_dest : "${final_dest:=$default_local_dest}" + local path_validation_attempts=0 + local max_attempts=5 + while true; do + ((path_validation_attempts++)) + if (( path_validation_attempts > max_attempts )); then + printf "\n${C_RED}❌ Too many invalid attempts. Exiting restore mode.${C_RESET}\n"; return 1 + fi + if [[ "$final_dest" != "/" ]]; then final_dest="${final_dest%/}"; fi + local parent_dir; parent_dir=$(dirname -- "$final_dest") + if [[ "$final_dest" != /* ]]; then + printf "\n${C_RED}❌ Error: Please provide an absolute path (starting with '/').${C_RESET}\n" + elif [[ -e "$final_dest" && ! -d "$final_dest" ]]; then + printf "\n${C_RED}❌ Error: The destination '%s' exists but is a file. Please choose a different path.${C_RESET}\n" "$final_dest" + elif [[ -e "$parent_dir" && ! -w "$parent_dir" ]]; then + printf "\n${C_RED}❌ Error: The parent directory '%s' exists but is not writable.${C_RESET}\n" "$parent_dir" + elif [[ -d "$final_dest" ]]; then + printf "${C_GREEN}✅ Destination '%s' exists and is accessible.${C_RESET}\n" "$final_dest" + if [[ "$final_dest" != "$default_local_dest" && -z "$restore_path" ]]; then + local warning_msg="⚠️ WARNING: Custom destination directory already exists. Files may be overwritten." + printf "${C_YELLOW}%s${C_RESET}\n" "$warning_msg"; log_message "$warning_msg" + fi + break + else + printf "\n${C_YELLOW}⚠️ The destination '%s' does not exist.${C_RESET}\n" "$final_dest" + printf "${C_YELLOW}Choose an action:${C_RESET}\n" + PS3="Your choice: " + select action in "Create the destination path" "Enter a different path" "Cancel"; do + case "$action" in + "Create the destination path") + if mkdir -p "$final_dest"; then + printf "${C_GREEN}✅ Successfully created directory '%s'.${C_RESET}\n" "$final_dest" + if [[ "${is_full_directory_restore:-false}" == "true" ]]; then + chmod 700 "$final_dest"; log_message "Set permissions to 700 on newly created restore directory: $final_dest" + else + chmod 755 "$final_dest" + fi + break 2 + else + printf "\n${C_RED}❌ Failed to create directory '%s'. Check permissions.${C_RESET}\n" "$final_dest"; break + fi ;; + "Enter a different path") break ;; + "Cancel") echo "Restore cancelled by user."; return 0 ;; + *) echo "Invalid option. Please try again." ;; + esac + done + PS3="#? " + fi + if (( path_validation_attempts < max_attempts )); then + printf "\n${C_YELLOW}Please enter a new destination path: ${C_RESET}"; read -r final_dest + if [[ -z "$final_dest" ]]; then + final_dest="$default_local_dest"; printf "${C_DIM}Empty input, using default location: %s${C_RESET}\n" "$final_dest" + fi + fi + done local extra_rsync_opts=() local dest_user="" if [[ "$final_dest" == /home/* ]]; then @@ -436,60 +497,41 @@ run_restore_mode() { if [[ -n "$dest_user" ]] && id -u "$dest_user" &>/dev/null; then printf "${C_CYAN}ℹ️ Home directory detected. Restored files will be owned by '${dest_user}'.${C_RESET}\n" extra_rsync_opts+=("--chown=${dest_user}:${dest_user}") + chown "${dest_user}:${dest_user}" "$final_dest" 2>/dev/null || true else dest_user="" fi fi - local dest_created=false - if [[ ! -e "$final_dest" ]]; then - dest_created=true - fi - local dest_parent - dest_parent=$(dirname "$final_dest") - if ! mkdir -p "$dest_parent"; then - echo "❌ FATAL: Could not create parent destination directory '$dest_parent'. Aborting." >&2 - return 1 - fi - if [[ -n "$dest_user" ]]; then - chown "${dest_user}:${dest_user}" "$dest_parent" - fi - if [[ "$final_dest" != "$default_local_dest" && -d "$final_dest" && -z "$restore_path" ]]; then - local warning_msg="⚠️ WARNING: The custom destination directory '$final_dest' already exists. Files may be overwritten." - echo "$warning_msg"; log_message "$warning_msg" - fi - if [[ "$dest_created" == "true" && "${is_full_directory_restore:-false}" == "true" ]]; then - chmod 700 "$final_dest"; log_message "Set permissions to 700 on newly created restore directory: $final_dest" - fi - printf "Restore destination is set to: ${C_BOLD}%s${C_RESET}\n" "$final_dest" - printf "\n${C_BOLD}${C_YELLOW}--- PERFORMING DRY RUN. NO FILES WILL BE CHANGED. ---${C_RESET}\n" + printf "\n${C_BOLD}Restore Summary:${C_RESET}\n" + printf " Source: %s\n" "$item_for_display" + printf " Destination: ${C_BOLD}%s${C_RESET}\n" "$final_dest" + printf "\n${C_BOLD}${C_YELLOW}--- PERFORMING DRY RUN (NO CHANGES MADE) ---${C_RESET}\n" log_message "Starting restore dry-run of ${item_for_display} from ${full_remote_source} to ${final_dest}" - local rsync_restore_opts=(-avhi --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD") + local rsync_restore_opts=(-avhi --safe-links --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD") if ! rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" --dry-run "$full_remote_source" "$final_dest"; then - echo "❌ DRY RUN FAILED. Rsync reported an error. Aborting." >&2; return 1 + printf "${C_RED}❌ DRY RUN FAILED. Rsync reported an error. Check connectivity and permissions.${C_RESET}\n" >&2 + log_message "Restore dry-run failed for ${item_for_display}"; return 1 fi printf "${C_BOLD}${C_GREEN}--- DRY RUN COMPLETE ---${C_RESET}\n" - local confirmation while true; do - printf "\n${C_YELLOW}Are you sure you want to proceed with restoring %s to '%s'? [yes/no]: ${C_RESET}" "$item_for_display" "$final_dest" - read -r confirmation - - case "$confirmation" in - yes) break ;; - no) echo "Restore aborted by user." ; return 0 ;; - *) echo "Please answer yes or no." ;; + printf "\n${C_YELLOW}Proceed with restoring %s to '%s'? [yes/no]: ${C_RESET}" "$item_for_display" "$final_dest"; read -r confirmation + case "${confirmation,,}" in + yes|y) break ;; + no|n) echo "Restore cancelled by user."; return 0 ;; + *) echo "Please answer 'yes' or 'no'." ;; esac done - printf "\n${C_BOLD}--- PROCEEDING WITH RESTORE... ---${C_RESET}\n" - log_message "Starting REAL restore of ${item_for_display} from ${full_remote_source} to ${final_dest}" + printf "\n${C_BOLD}--- EXECUTING RESTORE ---${C_RESET}\n" + log_message "Starting actual restore of ${item_for_display} from ${full_remote_source} to ${final_dest}" if rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" "$full_remote_source" "$final_dest"; then log_message "Restore completed successfully." printf "${C_GREEN}✅ Restore of %s to '%s' completed successfully.${C_RESET}\n" "$item_for_display" "$final_dest" send_notification "Restore SUCCESS: ${HOSTNAME}" "white_check_mark" "${NTFY_PRIORITY_SUCCESS}" "success" "Successfully restored ${item_for_display} to ${final_dest}" else - log_message "Restore FAILED with rsync exit code $?." + local rsync_exit_code=$? + log_message "Restore FAILED with rsync exit code ${rsync_exit_code}." printf "${C_RED}❌ Restore FAILED. Check the rsync output and log for details.${C_RESET}\n" - send_notification "Restore FAILED: ${HOSTNAME}" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Restore of ${item_for_display} to ${final_dest} failed." - return 1 + send_notification "Restore FAILED: ${HOSTNAME}" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Restore of ${item_for_display} to ${final_dest} failed (exit code: ${rsync_exit_code})"; return 1 fi } run_recycle_bin_cleanup() { From 7618d71ae2bb9dc7800f0e65c75c61d0cc315da8 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:23:33 +0100 Subject: [PATCH 2/9] version bump --- backup.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backup.conf b/backup.conf index 46c07ad..0b95fc0 100644 --- a/backup.conf +++ b/backup.conf @@ -1,5 +1,5 @@ # ================================================================= -# Configuration for rsync Backup Script v0.32 +# Configuration for rsync Backup Script v0.33 # ================================================================= # !! IMPORTANT !! Set file permissions to 600 (chmod 600 backup.conf) From bf3cd27c96a23fbc786fc3e807ca06a77243b02b Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Fri, 15 Aug 2025 00:17:55 +0100 Subject: [PATCH 3/9] improve restore --- backup_script.sh | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/backup_script.sh b/backup_script.sh index ff17884..fcf91b8 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -379,7 +379,7 @@ run_restore_mode() { local remote_date_path="${remote_recycle_path}/${date_choice}" printf "${C_BOLD}--- Files available from ${date_choice} (showing first 20) ---${C_RESET}\n" local remote_listing_source="${BOX_ADDR}:${remote_date_path}/" - rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" /dev/null | head -n 20 || echo "No files found for this date." + rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" . 2>/dev/null | head -n 20 || echo "No files found for this date." printf "${C_BOLD}--------------------------------------------------------${C_RESET}\n" printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}"; read -r specific_path if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then @@ -388,7 +388,8 @@ run_restore_mode() { specific_path=$(echo "$specific_path" | sed 's#^/##') if [[ -z "$specific_path" ]]; then echo "❌ Path cannot be empty. Aborting."; return 1; fi full_remote_source="${BOX_ADDR}:${remote_date_path}/${specific_path}" - if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" /dev/null >/dev/null 2>&1; then + # FIX: Changed destination from /dev/null to a temporary directory '.' for validation. + if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" . >/dev/null 2>&1; then echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2; return 1 fi default_local_dest="/${specific_path}"; item_for_display="(from Recycle Bin) '${specific_path}'" @@ -427,13 +428,13 @@ run_restore_mode() { fi fi local final_dest - printf "\n%s\n" "${C_BOLD}--------------------------------------------------------" - printf "%s\n" " Restore Destination" - printf "%s\n" "--------------------------------------------------------${C_RESET}" - printf "%s\n\n" "Enter the absolute destination path for the restore." - printf "%s\n" "${C_YELLOW}Default (original location):${C_RESET}" - printf "${C_CYAN}%s${C_RESET}\n\n" "$default_local_dest" - printf "%s\n" "Press [Enter] to use the default path, or enter a new one." + echo -e "\n${C_BOLD}--------------------------------------------------------" + echo -e " Restore Destination" + echo -e "--------------------------------------------------------${C_RESET}" + echo -e "Enter the absolute destination path for the restore.\n" + echo -e "${C_YELLOW}Default (original location):${C_RESET}" + echo -e "${C_CYAN}${default_local_dest}${C_RESET}\n" + echo -e "Press [Enter] to use the default path, or enter a new one." read -rp "> " final_dest : "${final_dest:=$default_local_dest}" local path_validation_attempts=0 From fdc7b1d130561d9f2e4b1454ce9ff3f8d84a0617 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Fri, 15 Aug 2025 00:23:23 +0100 Subject: [PATCH 4/9] Improved restore function --- backup_script.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/backup_script.sh b/backup_script.sh index fcf91b8..d4542f9 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -388,7 +388,6 @@ run_restore_mode() { specific_path=$(echo "$specific_path" | sed 's#^/##') if [[ -z "$specific_path" ]]; then echo "❌ Path cannot be empty. Aborting."; return 1; fi full_remote_source="${BOX_ADDR}:${remote_date_path}/${specific_path}" - # FIX: Changed destination from /dev/null to a temporary directory '.' for validation. if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" . >/dev/null 2>&1; then echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2; return 1 fi From 2916f969aab2b69fd1cc71b8038e46dd722e59e1 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Fri, 15 Aug 2025 00:45:17 +0100 Subject: [PATCH 5/9] improved restore formatting --- backup_script.sh | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/backup_script.sh b/backup_script.sh index d4542f9..0412266 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -336,8 +336,11 @@ run_preflight_checks() { if [[ "$test_mode" == "true" ]]; then printf "${C_GREEN}✅ Local disk space OK.${C_RESET}\n"; fi fi } +print_header() { + printf "\n%b--- %s ---%b\n" "${C_BOLD}" "$1" "${C_RESET}" +} run_restore_mode() { - printf "${C_BOLD}${C_CYAN}--- RESTORE MODE ACTIVATED ---${C_RESET}\n" + print_header "RESTORE MODE ACTIVATED" run_preflight_checks "restore" local DIRS_ARRAY; read -ra DIRS_ARRAY <<< "$BACKUP_DIRS" local RECYCLE_OPTION="[ Restore from Recycle Bin ]" @@ -347,40 +350,46 @@ run_restore_mode() { fi all_options+=("Cancel") printf "${C_YELLOW}Available backup sets to restore from:${C_RESET}\n" + PS3="Your choice: " select dir_choice in "${all_options[@]}"; do if [[ -n "$dir_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi done + PS3="#? " local full_remote_source="" local default_local_dest="" local item_for_display="" local restore_path="" local is_full_directory_restore=false if [[ "$dir_choice" == "$RECYCLE_OPTION" ]]; then - printf "${C_BOLD}${C_CYAN}--- Browse Recycle Bin ---${C_RESET}\n" + print_header "Browse Recycle Bin" local remote_recycle_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" local date_folders; date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true local valid_folders=() for f in $date_folders; do - if [[ "$f" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$ ]]; then - valid_folders+=( "$f" ) - fi + case "$f" in + [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9]) + valid_folders+=( "$f" ) + ;; + esac done date_folders=("${valid_folders[@]}") if [[ ${#date_folders[@]} -eq 0 ]]; then echo "❌ No validly-named backup folders found in the recycle bin." >&2; return 1 fi printf "${C_YELLOW}Select a backup run (date_time) to browse:${C_RESET}\n" + PS3="Your choice: " select date_choice in "${date_folders[@]}" "Cancel"; do if [[ "$date_choice" == "Cancel" ]]; then echo "Restore cancelled."; return 0; elif [[ -n "$date_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi done + PS3="#? " local remote_date_path="${remote_recycle_path}/${date_choice}" - printf "${C_BOLD}--- Files available from ${date_choice} (showing first 20) ---${C_RESET}\n" + print_header "Files available from ${date_choice} (showing first 20)" local remote_listing_source="${BOX_ADDR}:${remote_date_path}/" rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" . 2>/dev/null | head -n 20 || echo "No files found for this date." - printf "${C_BOLD}--------------------------------------------------------${C_RESET}\n" + printf "%b--------------------------------------------------------%b\n" "${C_BOLD}" "${C_RESET}" printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}"; read -r specific_path if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 @@ -401,7 +410,7 @@ run_restore_mode() { case "$choice" in entire) is_full_directory_restore=true; break ;; specific) - printf -v specific_path_prompt "Enter the path relative to '%s' to restore: " "$dir_choice"; printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt"; read -er specific_path + printf -v specific_path_prompt "Enter the path relative to '%s' to restore (e.g., subfolder/file.txt): " "$dir_choice"; printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt"; read -er specific_path if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 fi @@ -427,13 +436,11 @@ run_restore_mode() { fi fi local final_dest - echo -e "\n${C_BOLD}--------------------------------------------------------" - echo -e " Restore Destination" - echo -e "--------------------------------------------------------${C_RESET}" - echo -e "Enter the absolute destination path for the restore.\n" - echo -e "${C_YELLOW}Default (original location):${C_RESET}" - echo -e "${C_CYAN}${default_local_dest}${C_RESET}\n" - echo -e "Press [Enter] to use the default path, or enter a new one." + print_header "Restore Destination" + printf "Enter the absolute destination path for the restore.\n\n" + printf "%bDefault (original location):%b\n" "${C_YELLOW}" "${C_RESET}" + printf "%b%s%b\n\n" "${C_CYAN}" "$default_local_dest" "${C_RESET}" + printf "Press [Enter] to use the default path, or enter a new one.\n" read -rp "> " final_dest : "${final_dest:=$default_local_dest}" local path_validation_attempts=0 @@ -502,17 +509,17 @@ run_restore_mode() { dest_user="" fi fi - printf "\n${C_BOLD}Restore Summary:${C_RESET}\n" + print_header "Restore Summary" printf " Source: %s\n" "$item_for_display" - printf " Destination: ${C_BOLD}%s${C_RESET}\n" "$final_dest" - printf "\n${C_BOLD}${C_YELLOW}--- PERFORMING DRY RUN (NO CHANGES MADE) ---${C_RESET}\n" + printf " Destination: %b%s%b\n" "${C_BOLD}" "$final_dest" "${C_RESET}" + print_header "PERFORMING DRY RUN (NO CHANGES MADE)" log_message "Starting restore dry-run of ${item_for_display} from ${full_remote_source} to ${final_dest}" local rsync_restore_opts=(-avhi --safe-links --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD") if ! rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" --dry-run "$full_remote_source" "$final_dest"; then printf "${C_RED}❌ DRY RUN FAILED. Rsync reported an error. Check connectivity and permissions.${C_RESET}\n" >&2 log_message "Restore dry-run failed for ${item_for_display}"; return 1 fi - printf "${C_BOLD}${C_GREEN}--- DRY RUN COMPLETE ---${C_RESET}\n" + print_header "DRY RUN COMPLETE" while true; do printf "\n${C_YELLOW}Proceed with restoring %s to '%s'? [yes/no]: ${C_RESET}" "$item_for_display" "$final_dest"; read -r confirmation case "${confirmation,,}" in @@ -521,7 +528,7 @@ run_restore_mode() { *) echo "Please answer 'yes' or 'no'." ;; esac done - printf "\n${C_BOLD}--- EXECUTING RESTORE ---${C_RESET}\n" + print_header "EXECUTING RESTORE" log_message "Starting actual restore of ${item_for_display} from ${full_remote_source} to ${final_dest}" if rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" "$full_remote_source" "$final_dest"; then log_message "Restore completed successfully." From e08e1299b4bcc9f68973160541a8abce1e853386 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:10:31 +0100 Subject: [PATCH 6/9] improved specific file restore --- backup_script.sh | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/backup_script.sh b/backup_script.sh index 0412266..4f15325 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -355,7 +355,7 @@ run_restore_mode() { if [[ -n "$dir_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi done - PS3="#? " + PS3="#? " local full_remote_source="" local default_local_dest="" local item_for_display="" @@ -363,19 +363,10 @@ run_restore_mode() { local is_full_directory_restore=false if [[ "$dir_choice" == "$RECYCLE_OPTION" ]]; then print_header "Browse Recycle Bin" + local date_folders=() local remote_recycle_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" - local date_folders; date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true - local valid_folders=() - for f in $date_folders; do - case "$f" in - [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9]) - valid_folders+=( "$f" ) - ;; - esac - done - date_folders=("${valid_folders[@]}") + mapfile -t date_folders < <(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$') if [[ ${#date_folders[@]} -eq 0 ]]; then - echo "❌ No validly-named backup folders found in the recycle bin." >&2; return 1 fi printf "${C_YELLOW}Select a backup run (date_time) to browse:${C_RESET}\n" PS3="Your choice: " @@ -410,6 +401,11 @@ run_restore_mode() { case "$choice" in entire) is_full_directory_restore=true; break ;; specific) + local relative_path_browse="${dir_choice#*./}" + local remote_browse_source="${REMOTE_TARGET}${relative_path_browse}" + print_header "Files available in ${dir_choice} (showing first 20)" + rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_browse_source" . 2>/dev/null | head -n 20 || echo "No files found for this backup set." + printf "%b--------------------------------------------------------%b\n" "${C_BOLD}" "${C_RESET}" printf -v specific_path_prompt "Enter the path relative to '%s' to restore (e.g., subfolder/file.txt): " "$dir_choice"; printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt"; read -er specific_path if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 From b5ba63ee742537902cb3433390d78b53d8480666 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Fri, 15 Aug 2025 02:04:31 +0100 Subject: [PATCH 7/9] Improve restore function --- backup_script.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backup_script.sh b/backup_script.sh index 4f15325..12e83e2 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -355,7 +355,7 @@ run_restore_mode() { if [[ -n "$dir_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi done - PS3="#? " + PS3="#? " local full_remote_source="" local default_local_dest="" local item_for_display="" @@ -543,10 +543,10 @@ run_recycle_bin_cleanup() { local remote_cleanup_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" local list_command="ls -1 \"$remote_cleanup_path\"" local all_folders - all_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "$list_command" 2>> "${LOG_FILE:-/dev/null}") || { + if ! all_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "$list_command" 2>> "${LOG_FILE:-/dev/null}"); then log_message "Recycle bin not found or unable to list contents. Nothing to clean." return 0 - } + fi if [[ -z "$all_folders" ]]; then log_message "No daily folders in recycle bin to check." return 0 From 9b3b88ccea49698d91d20e367833ef2aa386eeb0 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 15 Aug 2025 02:20:28 +0100 Subject: [PATCH 8/9] Fix: shellcheck --- backup_script.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/backup_script.sh b/backup_script.sh index 12e83e2..5fd7c2f 100644 --- a/backup_script.sh +++ b/backup_script.sh @@ -1,5 +1,5 @@ #!/bin/bash -# ===================== v0.33 - 2025.08.14 ======================== +# ===================== v0.33 - 2025.08.15 ======================== # # Example backup.conf: # BACKUP_DIRS="/home/user/test/./ /var/www/./" @@ -366,8 +366,10 @@ run_restore_mode() { local date_folders=() local remote_recycle_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" mapfile -t date_folders < <(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$') - if [[ ${#date_folders[@]} -eq 0 ]]; then - fi + if [[ ${#date_folders[@]} -eq 0 ]]; then + printf "${C_YELLOW}❌ The remote recycle bin is empty or contains no valid backup folders.${C_RESET}\n" + return 1 + fi printf "${C_YELLOW}Select a backup run (date_time) to browse:${C_RESET}\n" PS3="Your choice: " select date_choice in "${date_folders[@]}" "Cancel"; do @@ -694,7 +696,8 @@ for dir in "${DIRS_ARRAY[@]}"; do RSYNC_EXIT_CODE=${PIPESTATUS[0]} else RSYNC_OPTS+=(--info=stats2) - nice -n 19 ionice -c 3 rsync "${RSYNC_OPTS[@]}" "$dir" "$REMOTE_TARGET" > "$RSYNC_LOG_TMP" 2>&1 || RSYNC_EXIT_CODE=$? + nice -n 19 ionice -c 3 rsync "${RSYNC_OPTS[@]}" "$dir" "$REMOTE_TARGET" > "$RSYNC_LOG_TMP" 2>&1 + RSYNC_EXIT_CODE=$? fi cat "$RSYNC_LOG_TMP" >> "$LOG_FILE"; full_rsync_output+=$'\n'"$(<"$RSYNC_LOG_TMP")" rm -f "$RSYNC_LOG_TMP" From 9ba58c30a904aaf28e23a03bd80fe7cee81c3c52 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Fri, 15 Aug 2025 02:29:03 +0100 Subject: [PATCH 9/9] Updated to v0.33 --- README.md | 214 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 131 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 8f1f105..c78741f 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ To run the backup automatically, edit the root crontab. ```ini # ================================================================= -# Configuration for rsync Backup Script v0.32 +# Configuration for rsync Backup Script v0.33 # ================================================================= # !! IMPORTANT !! Set file permissions to 600 (chmod 600 backup.conf) @@ -310,7 +310,7 @@ END_EXCLUDES ```bash #!/bin/bash -# ===================== v0.32 - 2025.08.13 ======================== +# ===================== v0.33 - 2025.08.15 ======================== # # ================================================================= # SCRIPT INITIALIZATION & SETUP @@ -616,8 +616,11 @@ run_preflight_checks() { if [[ "$test_mode" == "true" ]]; then printf "${C_GREEN}✅ Local disk space OK.${C_RESET}\n"; fi fi } +print_header() { + printf "\n%b--- %s ---%b\n" "${C_BOLD}" "$1" "${C_RESET}" +} run_restore_mode() { - printf "${C_BOLD}${C_CYAN}--- RESTORE MODE ACTIVATED ---${C_RESET}\n" + print_header "RESTORE MODE ACTIVATED" run_preflight_checks "restore" local DIRS_ARRAY; read -ra DIRS_ARRAY <<< "$BACKUP_DIRS" local RECYCLE_OPTION="[ Restore from Recycle Bin ]" @@ -627,88 +630,151 @@ run_restore_mode() { fi all_options+=("Cancel") printf "${C_YELLOW}Available backup sets to restore from:${C_RESET}\n" + PS3="Your choice: " select dir_choice in "${all_options[@]}"; do if [[ -n "$dir_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi done + PS3="#? " local full_remote_source="" local default_local_dest="" local item_for_display="" local restore_path="" local is_full_directory_restore=false if [[ "$dir_choice" == "$RECYCLE_OPTION" ]]; then - printf "${C_BOLD}${C_CYAN}--- Browse Recycle Bin ---${C_RESET}\n" + print_header "Browse Recycle Bin" + local date_folders=() local remote_recycle_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" - local date_folders - date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true - if [[ -z "$date_folders" ]]; then - echo "❌ No dated folders found in the recycle bin. Nothing to restore." >&2 - return 1 - fi + mapfile -t date_folders < <(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$') + if [[ ${#date_folders[@]} -eq 0 ]]; then + printf "${C_YELLOW}❌ The remote recycle bin is empty or contains no valid backup folders.${C_RESET}\n" + return 1 + fi printf "${C_YELLOW}Select a backup run (date_time) to browse:${C_RESET}\n" - select date_choice in $date_folders "Cancel"; do + PS3="Your choice: " + select date_choice in "${date_folders[@]}" "Cancel"; do if [[ "$date_choice" == "Cancel" ]]; then echo "Restore cancelled."; return 0; elif [[ -n "$date_choice" ]]; then break; else echo "Invalid selection. Please try again."; fi done + PS3="#? " local remote_date_path="${remote_recycle_path}/${date_choice}" - printf "${C_BOLD}--- Files available from ${date_choice} (showing first 20) ---${C_RESET}\n" + print_header "Files available from ${date_choice} (showing first 20)" local remote_listing_source="${BOX_ADDR}:${remote_date_path}/" rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" . 2>/dev/null | head -n 20 || echo "No files found for this date." - printf "${C_BOLD}--------------------------------------------------------${C_RESET}\n" - printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}" - read -r specific_path + printf "%b--------------------------------------------------------%b\n" "${C_BOLD}" "${C_RESET}" + printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}"; read -r specific_path + if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then + echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 + fi specific_path=$(echo "$specific_path" | sed 's#^/##') if [[ -z "$specific_path" ]]; then echo "❌ Path cannot be empty. Aborting."; return 1; fi full_remote_source="${BOX_ADDR}:${remote_date_path}/${specific_path}" if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" . >/dev/null 2>&1; then - echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2 - return 1 + echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2; return 1 fi - default_local_dest="/${specific_path}" - item_for_display="(from Recycle Bin) '${specific_path}'" + default_local_dest="/${specific_path}"; item_for_display="(from Recycle Bin) '${specific_path}'" elif [[ "$dir_choice" == "Cancel" ]]; then - echo "Restore cancelled." - return 0 + echo "Restore cancelled."; return 0 else item_for_display="the entire directory '${dir_choice}'" while true; do - printf "\n${C_YELLOW}Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET}" - read -r choice + printf "\n${C_YELLOW}Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET}"; read -r choice case "$choice" in - entire) - is_full_directory_restore=true - break - ;; + entire) is_full_directory_restore=true; break ;; specific) - local specific_path_prompt - printf -v specific_path_prompt "Enter the path relative to '%s' to restore: " "$dir_choice" - printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt" - read -er specific_path + local relative_path_browse="${dir_choice#*./}" + local remote_browse_source="${REMOTE_TARGET}${relative_path_browse}" + print_header "Files available in ${dir_choice} (showing first 20)" + rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_browse_source" . 2>/dev/null | head -n 20 || echo "No files found for this backup set." + printf "%b--------------------------------------------------------%b\n" "${C_BOLD}" "${C_RESET}" + printf -v specific_path_prompt "Enter the path relative to '%s' to restore (e.g., subfolder/file.txt): " "$dir_choice"; printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt"; read -er specific_path + if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then + echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1 + fi specific_path=$(echo "$specific_path" | sed 's#^/##') if [[ -n "$specific_path" ]]; then - restore_path="$specific_path" - item_for_display="'$restore_path' from '${dir_choice}'" - break + restore_path="$specific_path"; item_for_display="'$restore_path' from '${dir_choice}'"; break else echo "Path cannot be empty. Please try again or choose 'entire'." - fi - ;; + fi ;; *) echo "Invalid choice. Please answer 'entire' or 'specific'." ;; esac done local relative_path="${dir_choice#*./}" - full_remote_source="${REMOTE_TARGET}${relative_path}${restore_path}" + local remote_base="${REMOTE_TARGET%/}" + full_remote_source="${remote_base}/${relative_path#/}" + if [[ -n "$restore_path" ]]; then + full_remote_source="${full_remote_source%/}/${restore_path#/}" + fi if [[ -n "$restore_path" ]]; then - default_local_dest=$(echo "${dir_choice}${restore_path}" | sed 's#/\./#/#') + default_local_dest=$(echo "${dir_choice}${restore_path}" | sed 's#/\./#/#g') else - default_local_dest=$(echo "$dir_choice" | sed 's#/\./#/#') + default_local_dest=$(echo "$dir_choice" | sed 's#/\./#/#g') fi fi - local final_dest - printf "\n${C_YELLOW}Enter the destination path.\n${C_DIM}Press [Enter] to use the original location (%s):${C_RESET} " "$default_local_dest" - read -r final_dest + local final_dest + print_header "Restore Destination" + printf "Enter the absolute destination path for the restore.\n\n" + printf "%bDefault (original location):%b\n" "${C_YELLOW}" "${C_RESET}" + printf "%b%s%b\n\n" "${C_CYAN}" "$default_local_dest" "${C_RESET}" + printf "Press [Enter] to use the default path, or enter a new one.\n" + read -rp "> " final_dest : "${final_dest:=$default_local_dest}" + local path_validation_attempts=0 + local max_attempts=5 + while true; do + ((path_validation_attempts++)) + if (( path_validation_attempts > max_attempts )); then + printf "\n${C_RED}❌ Too many invalid attempts. Exiting restore mode.${C_RESET}\n"; return 1 + fi + if [[ "$final_dest" != "/" ]]; then final_dest="${final_dest%/}"; fi + local parent_dir; parent_dir=$(dirname -- "$final_dest") + if [[ "$final_dest" != /* ]]; then + printf "\n${C_RED}❌ Error: Please provide an absolute path (starting with '/').${C_RESET}\n" + elif [[ -e "$final_dest" && ! -d "$final_dest" ]]; then + printf "\n${C_RED}❌ Error: The destination '%s' exists but is a file. Please choose a different path.${C_RESET}\n" "$final_dest" + elif [[ -e "$parent_dir" && ! -w "$parent_dir" ]]; then + printf "\n${C_RED}❌ Error: The parent directory '%s' exists but is not writable.${C_RESET}\n" "$parent_dir" + elif [[ -d "$final_dest" ]]; then + printf "${C_GREEN}✅ Destination '%s' exists and is accessible.${C_RESET}\n" "$final_dest" + if [[ "$final_dest" != "$default_local_dest" && -z "$restore_path" ]]; then + local warning_msg="⚠️ WARNING: Custom destination directory already exists. Files may be overwritten." + printf "${C_YELLOW}%s${C_RESET}\n" "$warning_msg"; log_message "$warning_msg" + fi + break + else + printf "\n${C_YELLOW}⚠️ The destination '%s' does not exist.${C_RESET}\n" "$final_dest" + printf "${C_YELLOW}Choose an action:${C_RESET}\n" + PS3="Your choice: " + select action in "Create the destination path" "Enter a different path" "Cancel"; do + case "$action" in + "Create the destination path") + if mkdir -p "$final_dest"; then + printf "${C_GREEN}✅ Successfully created directory '%s'.${C_RESET}\n" "$final_dest" + if [[ "${is_full_directory_restore:-false}" == "true" ]]; then + chmod 700 "$final_dest"; log_message "Set permissions to 700 on newly created restore directory: $final_dest" + else + chmod 755 "$final_dest" + fi + break 2 + else + printf "\n${C_RED}❌ Failed to create directory '%s'. Check permissions.${C_RESET}\n" "$final_dest"; break + fi ;; + "Enter a different path") break ;; + "Cancel") echo "Restore cancelled by user."; return 0 ;; + *) echo "Invalid option. Please try again." ;; + esac + done + PS3="#? " + fi + if (( path_validation_attempts < max_attempts )); then + printf "\n${C_YELLOW}Please enter a new destination path: ${C_RESET}"; read -r final_dest + if [[ -z "$final_dest" ]]; then + final_dest="$default_local_dest"; printf "${C_DIM}Empty input, using default location: %s${C_RESET}\n" "$final_dest" + fi + fi + done local extra_rsync_opts=() local dest_user="" if [[ "$final_dest" == /home/* ]]; then @@ -716,60 +782,41 @@ run_restore_mode() { if [[ -n "$dest_user" ]] && id -u "$dest_user" &>/dev/null; then printf "${C_CYAN}ℹ️ Home directory detected. Restored files will be owned by '${dest_user}'.${C_RESET}\n" extra_rsync_opts+=("--chown=${dest_user}:${dest_user}") + chown "${dest_user}:${dest_user}" "$final_dest" 2>/dev/null || true else dest_user="" fi fi - local dest_created=false - if [[ ! -e "$final_dest" ]]; then - dest_created=true - fi - local dest_parent - dest_parent=$(dirname "$final_dest") - if ! mkdir -p "$dest_parent"; then - echo "❌ FATAL: Could not create parent destination directory '$dest_parent'. Aborting." >&2 - return 1 - fi - if [[ -n "$dest_user" ]]; then - chown "${dest_user}:${dest_user}" "$dest_parent" - fi - if [[ "$final_dest" != "$default_local_dest" && -d "$final_dest" && -z "$restore_path" ]]; then - local warning_msg="⚠️ WARNING: The custom destination directory '$final_dest' already exists. Files may be overwritten." - echo "$warning_msg"; log_message "$warning_msg" - fi - if [[ "$dest_created" == "true" && "${is_full_directory_restore:-false}" == "true" ]]; then - chmod 700 "$final_dest"; log_message "Set permissions to 700 on newly created restore directory: $final_dest" - fi - printf "Restore destination is set to: ${C_BOLD}%s${C_RESET}\n" "$final_dest" - printf "\n${C_BOLD}${C_YELLOW}--- PERFORMING DRY RUN. NO FILES WILL BE CHANGED. ---${C_RESET}\n" + print_header "Restore Summary" + printf " Source: %s\n" "$item_for_display" + printf " Destination: %b%s%b\n" "${C_BOLD}" "$final_dest" "${C_RESET}" + print_header "PERFORMING DRY RUN (NO CHANGES MADE)" log_message "Starting restore dry-run of ${item_for_display} from ${full_remote_source} to ${final_dest}" - local rsync_restore_opts=(-avhi --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD") + local rsync_restore_opts=(-avhi --safe-links --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD") if ! rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" --dry-run "$full_remote_source" "$final_dest"; then - echo "❌ DRY RUN FAILED. Rsync reported an error. Aborting." >&2; return 1 + printf "${C_RED}❌ DRY RUN FAILED. Rsync reported an error. Check connectivity and permissions.${C_RESET}\n" >&2 + log_message "Restore dry-run failed for ${item_for_display}"; return 1 fi - printf "${C_BOLD}${C_GREEN}--- DRY RUN COMPLETE ---${C_RESET}\n" - local confirmation + print_header "DRY RUN COMPLETE" while true; do - printf "\n${C_YELLOW}Are you sure you want to proceed with restoring %s to '%s'? [yes/no]: ${C_RESET}" "$item_for_display" "$final_dest" - read -r confirmation - - case "$confirmation" in - yes) break ;; - no) echo "Restore aborted by user." ; return 0 ;; - *) echo "Please answer yes or no." ;; + printf "\n${C_YELLOW}Proceed with restoring %s to '%s'? [yes/no]: ${C_RESET}" "$item_for_display" "$final_dest"; read -r confirmation + case "${confirmation,,}" in + yes|y) break ;; + no|n) echo "Restore cancelled by user."; return 0 ;; + *) echo "Please answer 'yes' or 'no'." ;; esac done - printf "\n${C_BOLD}--- PROCEEDING WITH RESTORE... ---${C_RESET}\n" - log_message "Starting REAL restore of ${item_for_display} from ${full_remote_source} to ${final_dest}" + print_header "EXECUTING RESTORE" + log_message "Starting actual restore of ${item_for_display} from ${full_remote_source} to ${final_dest}" if rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" "$full_remote_source" "$final_dest"; then log_message "Restore completed successfully." printf "${C_GREEN}✅ Restore of %s to '%s' completed successfully.${C_RESET}\n" "$item_for_display" "$final_dest" send_notification "Restore SUCCESS: ${HOSTNAME}" "white_check_mark" "${NTFY_PRIORITY_SUCCESS}" "success" "Successfully restored ${item_for_display} to ${final_dest}" else - log_message "Restore FAILED with rsync exit code $?." + local rsync_exit_code=$? + log_message "Restore FAILED with rsync exit code ${rsync_exit_code}." printf "${C_RED}❌ Restore FAILED. Check the rsync output and log for details.${C_RESET}\n" - send_notification "Restore FAILED: ${HOSTNAME}" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Restore of ${item_for_display} to ${final_dest} failed." - return 1 + send_notification "Restore FAILED: ${HOSTNAME}" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Restore of ${item_for_display} to ${final_dest} failed (exit code: ${rsync_exit_code})"; return 1 fi } run_recycle_bin_cleanup() { @@ -778,10 +825,10 @@ run_recycle_bin_cleanup() { local remote_cleanup_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}" local list_command="ls -1 \"$remote_cleanup_path\"" local all_folders - all_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "$list_command" 2>> "${LOG_FILE:-/dev/null}") || { + if ! all_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "$list_command" 2>> "${LOG_FILE:-/dev/null}"); then log_message "Recycle bin not found or unable to list contents. Nothing to clean." return 0 - } + fi if [[ -z "$all_folders" ]]; then log_message "No daily folders in recycle bin to check." return 0 @@ -929,7 +976,8 @@ for dir in "${DIRS_ARRAY[@]}"; do RSYNC_EXIT_CODE=${PIPESTATUS[0]} else RSYNC_OPTS+=(--info=stats2) - nice -n 19 ionice -c 3 rsync "${RSYNC_OPTS[@]}" "$dir" "$REMOTE_TARGET" > "$RSYNC_LOG_TMP" 2>&1 || RSYNC_EXIT_CODE=$? + nice -n 19 ionice -c 3 rsync "${RSYNC_OPTS[@]}" "$dir" "$REMOTE_TARGET" > "$RSYNC_LOG_TMP" 2>&1 + RSYNC_EXIT_CODE=$? fi cat "$RSYNC_LOG_TMP" >> "$LOG_FILE"; full_rsync_output+=$'\n'"$(<"$RSYNC_LOG_TMP")" rm -f "$RSYNC_LOG_TMP"