Skip to content

Commit d567a8e

Browse files
authored
improve restore function
1 parent b37cbca commit d567a8e

File tree

1 file changed

+117
-75
lines changed

1 file changed

+117
-75
lines changed

backup_script.sh

Lines changed: 117 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
2-
# ===================== v0.32 - 2025.08.13 ========================
2+
# ===================== v0.33 - 2025.08.14 ========================
33
#
44
# Example backup.conf:
55
# BACKUP_DIRS="/home/user/test/./ /var/www/./"
@@ -359,137 +359,179 @@ run_restore_mode() {
359359
if [[ "$dir_choice" == "$RECYCLE_OPTION" ]]; then
360360
printf "${C_BOLD}${C_CYAN}--- Browse Recycle Bin ---${C_RESET}\n"
361361
local remote_recycle_path="${BOX_DIR%/}/${RECYCLE_BIN_DIR%/}"
362-
local date_folders
363-
date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true
364-
if [[ -z "$date_folders" ]]; then
365-
echo "❌ No dated folders found in the recycle bin. Nothing to restore." >&2
366-
return 1
362+
local date_folders; date_folders=$(ssh "${SSH_OPTS_ARRAY[@]}" "${SSH_DIRECT_OPTS[@]}" "$BOX_ADDR" "ls -1 \"$remote_recycle_path\"" 2>/dev/null) || true
363+
local valid_folders=()
364+
for f in $date_folders; do
365+
if [[ "$f" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$ ]]; then
366+
valid_folders+=( "$f" )
367+
fi
368+
done
369+
date_folders=("${valid_folders[@]}")
370+
if [[ ${#date_folders[@]} -eq 0 ]]; then
371+
echo "❌ No validly-named backup folders found in the recycle bin." >&2; return 1
367372
fi
368373
printf "${C_YELLOW}Select a backup run (date_time) to browse:${C_RESET}\n"
369-
select date_choice in $date_folders "Cancel"; do
374+
select date_choice in "${date_folders[@]}" "Cancel"; do
370375
if [[ "$date_choice" == "Cancel" ]]; then echo "Restore cancelled."; return 0;
371376
elif [[ -n "$date_choice" ]]; then break;
372377
else echo "Invalid selection. Please try again."; fi
373378
done
374379
local remote_date_path="${remote_recycle_path}/${date_choice}"
375380
printf "${C_BOLD}--- Files available from ${date_choice} (showing first 20) ---${C_RESET}\n"
376381
local remote_listing_source="${BOX_ADDR}:${remote_date_path}/"
377-
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."
382+
rsync -r -n --out-format='%n' -e "$SSH_CMD" "$remote_listing_source" /dev/null | head -n 20 || echo "No files found for this date."
378383
printf "${C_BOLD}--------------------------------------------------------${C_RESET}\n"
379-
printf "${C_YELLOW}Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET}"
380-
read -r specific_path
384+
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
385+
if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then
386+
echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1
387+
fi
381388
specific_path=$(echo "$specific_path" | sed 's#^/##')
382389
if [[ -z "$specific_path" ]]; then echo "❌ Path cannot be empty. Aborting."; return 1; fi
383390
full_remote_source="${BOX_ADDR}:${remote_date_path}/${specific_path}"
384-
if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" . >/dev/null 2>&1; then
385-
echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2
386-
return 1
391+
if ! rsync -r -n -e "$SSH_CMD" "$full_remote_source" /dev/null >/dev/null 2>&1; then
392+
echo "❌ ERROR: The path '${specific_path}' was not found in the recycle bin for ${date_choice}. Aborting." >&2; return 1
387393
fi
388-
default_local_dest="/${specific_path}"
389-
item_for_display="(from Recycle Bin) '${specific_path}'"
394+
default_local_dest="/${specific_path}"; item_for_display="(from Recycle Bin) '${specific_path}'"
390395
elif [[ "$dir_choice" == "Cancel" ]]; then
391-
echo "Restore cancelled."
392-
return 0
396+
echo "Restore cancelled."; return 0
393397
else
394398
item_for_display="the entire directory '${dir_choice}'"
395399
while true; do
396-
printf "\n${C_YELLOW}Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET}"
397-
read -r choice
400+
printf "\n${C_YELLOW}Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET}"; read -r choice
398401
case "$choice" in
399-
entire)
400-
is_full_directory_restore=true
401-
break
402-
;;
402+
entire) is_full_directory_restore=true; break ;;
403403
specific)
404-
local specific_path_prompt
405-
printf -v specific_path_prompt "Enter the path relative to '%s' to restore: " "$dir_choice"
406-
printf "${C_YELLOW}%s${C_RESET}" "$specific_path_prompt"
407-
read -er specific_path
404+
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
405+
if [[ "$specific_path" == /* || "$specific_path" =~ (^|/)\.\.(/|$) ]]; then
406+
echo "❌ Invalid restore path: must be relative and contain no '..'" >&2; return 1
407+
fi
408408
specific_path=$(echo "$specific_path" | sed 's#^/##')
409409
if [[ -n "$specific_path" ]]; then
410-
restore_path="$specific_path"
411-
item_for_display="'$restore_path' from '${dir_choice}'"
412-
break
410+
restore_path="$specific_path"; item_for_display="'$restore_path' from '${dir_choice}'"; break
413411
else
414412
echo "Path cannot be empty. Please try again or choose 'entire'."
415-
fi
416-
;;
413+
fi ;;
417414
*) echo "Invalid choice. Please answer 'entire' or 'specific'." ;;
418415
esac
419416
done
420417
local relative_path="${dir_choice#*./}"
421-
full_remote_source="${REMOTE_TARGET}${relative_path}${restore_path}"
418+
local remote_base="${REMOTE_TARGET%/}"
419+
full_remote_source="${remote_base}/${relative_path#/}"
420+
if [[ -n "$restore_path" ]]; then
421+
full_remote_source="${full_remote_source%/}/${restore_path#/}"
422+
fi
422423
if [[ -n "$restore_path" ]]; then
423-
default_local_dest=$(echo "${dir_choice}${restore_path}" | sed 's#/\./#/#')
424+
default_local_dest=$(echo "${dir_choice}${restore_path}" | sed 's#/\./#/#g')
424425
else
425-
default_local_dest=$(echo "$dir_choice" | sed 's#/\./#/#')
426+
default_local_dest=$(echo "$dir_choice" | sed 's#/\./#/#g')
426427
fi
427428
fi
428-
local final_dest
429-
printf "\n${C_YELLOW}Enter the destination path.\n${C_DIM}Press [Enter] to use the original location (%s):${C_RESET} " "$default_local_dest"
430-
read -r final_dest
429+
local final_dest
430+
printf "\n%s\n" "${C_BOLD}--------------------------------------------------------"
431+
printf "%s\n" " Restore Destination"
432+
printf "%s\n" "--------------------------------------------------------${C_RESET}"
433+
printf "%s\n\n" "Enter the absolute destination path for the restore."
434+
printf "%s\n" "${C_YELLOW}Default (original location):${C_RESET}"
435+
printf "${C_CYAN}%s${C_RESET}\n\n" "$default_local_dest"
436+
printf "%s\n" "Press [Enter] to use the default path, or enter a new one."
437+
read -rp "> " final_dest
431438
: "${final_dest:=$default_local_dest}"
439+
local path_validation_attempts=0
440+
local max_attempts=5
441+
while true; do
442+
((path_validation_attempts++))
443+
if (( path_validation_attempts > max_attempts )); then
444+
printf "\n${C_RED}❌ Too many invalid attempts. Exiting restore mode.${C_RESET}\n"; return 1
445+
fi
446+
if [[ "$final_dest" != "/" ]]; then final_dest="${final_dest%/}"; fi
447+
local parent_dir; parent_dir=$(dirname -- "$final_dest")
448+
if [[ "$final_dest" != /* ]]; then
449+
printf "\n${C_RED}❌ Error: Please provide an absolute path (starting with '/').${C_RESET}\n"
450+
elif [[ -e "$final_dest" && ! -d "$final_dest" ]]; then
451+
printf "\n${C_RED}❌ Error: The destination '%s' exists but is a file. Please choose a different path.${C_RESET}\n" "$final_dest"
452+
elif [[ -e "$parent_dir" && ! -w "$parent_dir" ]]; then
453+
printf "\n${C_RED}❌ Error: The parent directory '%s' exists but is not writable.${C_RESET}\n" "$parent_dir"
454+
elif [[ -d "$final_dest" ]]; then
455+
printf "${C_GREEN}✅ Destination '%s' exists and is accessible.${C_RESET}\n" "$final_dest"
456+
if [[ "$final_dest" != "$default_local_dest" && -z "$restore_path" ]]; then
457+
local warning_msg="⚠️ WARNING: Custom destination directory already exists. Files may be overwritten."
458+
printf "${C_YELLOW}%s${C_RESET}\n" "$warning_msg"; log_message "$warning_msg"
459+
fi
460+
break
461+
else
462+
printf "\n${C_YELLOW}⚠️ The destination '%s' does not exist.${C_RESET}\n" "$final_dest"
463+
printf "${C_YELLOW}Choose an action:${C_RESET}\n"
464+
PS3="Your choice: "
465+
select action in "Create the destination path" "Enter a different path" "Cancel"; do
466+
case "$action" in
467+
"Create the destination path")
468+
if mkdir -p "$final_dest"; then
469+
printf "${C_GREEN}✅ Successfully created directory '%s'.${C_RESET}\n" "$final_dest"
470+
if [[ "${is_full_directory_restore:-false}" == "true" ]]; then
471+
chmod 700 "$final_dest"; log_message "Set permissions to 700 on newly created restore directory: $final_dest"
472+
else
473+
chmod 755 "$final_dest"
474+
fi
475+
break 2
476+
else
477+
printf "\n${C_RED}❌ Failed to create directory '%s'. Check permissions.${C_RESET}\n" "$final_dest"; break
478+
fi ;;
479+
"Enter a different path") break ;;
480+
"Cancel") echo "Restore cancelled by user."; return 0 ;;
481+
*) echo "Invalid option. Please try again." ;;
482+
esac
483+
done
484+
PS3="#? "
485+
fi
486+
if (( path_validation_attempts < max_attempts )); then
487+
printf "\n${C_YELLOW}Please enter a new destination path: ${C_RESET}"; read -r final_dest
488+
if [[ -z "$final_dest" ]]; then
489+
final_dest="$default_local_dest"; printf "${C_DIM}Empty input, using default location: %s${C_RESET}\n" "$final_dest"
490+
fi
491+
fi
492+
done
432493
local extra_rsync_opts=()
433494
local dest_user=""
434495
if [[ "$final_dest" == /home/* ]]; then
435496
dest_user=$(echo "$final_dest" | cut -d/ -f3)
436497
if [[ -n "$dest_user" ]] && id -u "$dest_user" &>/dev/null; then
437498
printf "${C_CYAN}ℹ️ Home directory detected. Restored files will be owned by '${dest_user}'.${C_RESET}\n"
438499
extra_rsync_opts+=("--chown=${dest_user}:${dest_user}")
500+
chown "${dest_user}:${dest_user}" "$final_dest" 2>/dev/null || true
439501
else
440502
dest_user=""
441503
fi
442504
fi
443-
local dest_created=false
444-
if [[ ! -e "$final_dest" ]]; then
445-
dest_created=true
446-
fi
447-
local dest_parent
448-
dest_parent=$(dirname "$final_dest")
449-
if ! mkdir -p "$dest_parent"; then
450-
echo "❌ FATAL: Could not create parent destination directory '$dest_parent'. Aborting." >&2
451-
return 1
452-
fi
453-
if [[ -n "$dest_user" ]]; then
454-
chown "${dest_user}:${dest_user}" "$dest_parent"
455-
fi
456-
if [[ "$final_dest" != "$default_local_dest" && -d "$final_dest" && -z "$restore_path" ]]; then
457-
local warning_msg="⚠️ WARNING: The custom destination directory '$final_dest' already exists. Files may be overwritten."
458-
echo "$warning_msg"; log_message "$warning_msg"
459-
fi
460-
if [[ "$dest_created" == "true" && "${is_full_directory_restore:-false}" == "true" ]]; then
461-
chmod 700 "$final_dest"; log_message "Set permissions to 700 on newly created restore directory: $final_dest"
462-
fi
463-
printf "Restore destination is set to: ${C_BOLD}%s${C_RESET}\n" "$final_dest"
464-
printf "\n${C_BOLD}${C_YELLOW}--- PERFORMING DRY RUN. NO FILES WILL BE CHANGED. ---${C_RESET}\n"
505+
printf "\n${C_BOLD}Restore Summary:${C_RESET}\n"
506+
printf " Source: %s\n" "$item_for_display"
507+
printf " Destination: ${C_BOLD}%s${C_RESET}\n" "$final_dest"
508+
printf "\n${C_BOLD}${C_YELLOW}--- PERFORMING DRY RUN (NO CHANGES MADE) ---${C_RESET}\n"
465509
log_message "Starting restore dry-run of ${item_for_display} from ${full_remote_source} to ${final_dest}"
466-
local rsync_restore_opts=(-avhi --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD")
510+
local rsync_restore_opts=(-avhi --safe-links --progress --exclude-from="$EXCLUDE_FILE_TMP" -e "$SSH_CMD")
467511
if ! rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" --dry-run "$full_remote_source" "$final_dest"; then
468-
echo "❌ DRY RUN FAILED. Rsync reported an error. Aborting." >&2; return 1
512+
printf "${C_RED}❌ DRY RUN FAILED. Rsync reported an error. Check connectivity and permissions.${C_RESET}\n" >&2
513+
log_message "Restore dry-run failed for ${item_for_display}"; return 1
469514
fi
470515
printf "${C_BOLD}${C_GREEN}--- DRY RUN COMPLETE ---${C_RESET}\n"
471-
local confirmation
472516
while true; do
473-
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"
474-
read -r confirmation
475-
476-
case "$confirmation" in
477-
yes) break ;;
478-
no) echo "Restore aborted by user." ; return 0 ;;
479-
*) echo "Please answer yes or no." ;;
517+
printf "\n${C_YELLOW}Proceed with restoring %s to '%s'? [yes/no]: ${C_RESET}" "$item_for_display" "$final_dest"; read -r confirmation
518+
case "${confirmation,,}" in
519+
yes|y) break ;;
520+
no|n) echo "Restore cancelled by user."; return 0 ;;
521+
*) echo "Please answer 'yes' or 'no'." ;;
480522
esac
481523
done
482-
printf "\n${C_BOLD}--- PROCEEDING WITH RESTORE... ---${C_RESET}\n"
483-
log_message "Starting REAL restore of ${item_for_display} from ${full_remote_source} to ${final_dest}"
524+
printf "\n${C_BOLD}--- EXECUTING RESTORE ---${C_RESET}\n"
525+
log_message "Starting actual restore of ${item_for_display} from ${full_remote_source} to ${final_dest}"
484526
if rsync "${rsync_restore_opts[@]}" "${extra_rsync_opts[@]}" "$full_remote_source" "$final_dest"; then
485527
log_message "Restore completed successfully."
486528
printf "${C_GREEN}✅ Restore of %s to '%s' completed successfully.${C_RESET}\n" "$item_for_display" "$final_dest"
487529
send_notification "Restore SUCCESS: ${HOSTNAME}" "white_check_mark" "${NTFY_PRIORITY_SUCCESS}" "success" "Successfully restored ${item_for_display} to ${final_dest}"
488530
else
489-
log_message "Restore FAILED with rsync exit code $?."
531+
local rsync_exit_code=$?
532+
log_message "Restore FAILED with rsync exit code ${rsync_exit_code}."
490533
printf "${C_RED}❌ Restore FAILED. Check the rsync output and log for details.${C_RESET}\n"
491-
send_notification "Restore FAILED: ${HOSTNAME}" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Restore of ${item_for_display} to ${final_dest} failed."
492-
return 1
534+
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
493535
fi
494536
}
495537
run_recycle_bin_cleanup() {

0 commit comments

Comments
 (0)