@@ -184,7 +184,7 @@ To run the backup automatically, edit the root crontab.
184
184
185
185
```ini
186
186
# =================================================================
187
- # Configuration for rsync Backup Script v0.28
187
+ # Configuration for rsync Backup Script v0.29
188
188
# =================================================================
189
189
# !! IMPORTANT !! Set file permissions to 600 (chmod 600 backup.conf)
190
190
@@ -301,7 +301,7 @@ END_EXCLUDES
301
301
302
302
` ` ` bash
303
303
#! /bin/bash
304
- # ===================== v0.28 - 2025.08.12 ========================
304
+ # ===================== v0.29 - 2025.08.13 ========================
305
305
#
306
306
# =================================================================
307
307
# SCRIPT INITIALIZATION & SETUP
@@ -311,6 +311,25 @@ umask 077
311
311
312
312
HOSTNAME=$( hostname -s)
313
313
314
+ # --- Color Palette ---
315
+ if [ -t 1 ]; then
316
+ C_RESET=' \e[0m'
317
+ C_BOLD=' \e[1m'
318
+ C_DIM=' \e[2m'
319
+ C_RED=' \e[0;31m'
320
+ C_GREEN=' \e[0;32m'
321
+ C_YELLOW=' \e[0;33m'
322
+ C_CYAN=' \e[0;36m'
323
+ else
324
+ C_RESET=' '
325
+ C_BOLD=' '
326
+ C_DIM=' '
327
+ C_RED=' '
328
+ C_GREEN=' '
329
+ C_YELLOW=' '
330
+ C_CYAN=' '
331
+ fi
332
+
314
333
# Check if the script is being run as root
315
334
if (( EUID != 0 )) ; then
316
335
echo " ❌ This script must be run as root or with sudo." >&2
@@ -514,20 +533,19 @@ run_preflight_checks() {
514
533
local mode=${1:- backup} ; local test_mode=false
515
534
if [[ " $mode " == " test" ]]; then test_mode=true; fi
516
535
local check_failed=false
517
- if [[ " $test_mode " == " true" ]]; then echo " --- Checking required commands..." ; fi
536
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_BOLD} --- Checking required commands...${C_RESET} \n " ; fi
518
537
for cmd in " ${REQUIRED_CMDS[@]} " ; do
519
538
if ! command -v " $cmd " & > /dev/null; then echo " ❌ FATAL: Required command '$cmd ' not found." >&2 ; check_failed=true; fi
520
539
done
521
540
if [[ " $check_failed " == " true" ]]; then exit 10; fi
522
- if [[ " $test_mode " == " true" ]]; then echo " ✅ All required commands are present." ; fi
523
- if [[ " $test_mode " == " true" ]]; then echo " --- Checking SSH connectivity..." ; fi
524
-
541
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_GREEN} ✅ All required commands are present.${C_RESET} \n" ; fi
542
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_BOLD} --- Checking SSH connectivity...${C_RESET} \n" ; fi
525
543
# Quick preflight connectivity "ping": short 10s timeout for fail-fast behaviour
526
544
if ! ssh " ${SSH_OPTS_ARRAY[@]} " -o BatchMode=yes -o ConnectTimeout=10 " $BOX_ADDR " ' exit' 2> /dev/null; then
527
545
local err_msg=" Unable to SSH into $BOX_ADDR . Check keys and connectivity."
528
546
if [[ " $test_mode " == " true" ]]; then echo " ❌ $err_msg " ; else send_notification " ❌ SSH FAILED: ${HOSTNAME} " " x" " ${NTFY_PRIORITY_FAILURE} " " failure" " $err_msg " ; fi ; exit 6
529
547
fi
530
- if [[ " $test_mode " == " true" ]]; then echo " ✅ SSH connectivity OK." ; fi
548
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_GREEN} ✅ SSH connectivity OK.${C_RESET} \n " ; fi
531
549
if [[ " ${RECYCLE_BIN_ENABLED:- false} " == " true" ]]; then
532
550
local remote_recycle_path=" ${BOX_DIR}${RECYCLE_BIN_DIR} "
533
551
if ! ssh " ${SSH_OPTS_ARRAY[@]} " -o BatchMode=yes -o ConnectTimeout=10 " $BOX_ADDR " " ls -d \" $remote_recycle_path \" " > /dev/null 2>&1 ; then
@@ -538,7 +556,7 @@ run_preflight_checks() {
538
556
fi
539
557
fi
540
558
if [[ " $mode " != " restore" ]]; then
541
- if [[ " $test_mode " == " true" ]]; then echo " --- Checking backup directories..." ; fi
559
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_BOLD} --- Checking backup directories...${C_RESET} \n " ; fi
542
560
local DIRS_ARRAY; read -ra DIRS_ARRAY <<< " $BACKUP_DIRS"
543
561
for dir in " ${DIRS_ARRAY[@]} " ; do
544
562
if [[ ! -d " $dir " ]] || [[ " $dir " != * / ]]; then
@@ -559,21 +577,21 @@ run_preflight_checks() {
559
577
if [[ " $test_mode " == " true" ]]; then echo " ❌ FATAL: $err_msg " ; else send_notification " ❌ Backup FAILED: ${HOSTNAME} " " x" " ${NTFY_PRIORITY_FAILURE} " " failure" " FATAL: $err_msg " ; fi ; exit 2
560
578
fi
561
579
done
562
- if [[ " $test_mode " == " true" ]]; then echo " ✅ All backup directories are valid." ; fi
563
- if [[ " $test_mode " == " true" ]]; then echo " --- Checking local disk space..." ; fi
564
- local required_space_kb=102400 # 100MB in KB
580
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_GREEN} ✅ All backup directories are valid.${C_RESET} \n " ; fi
581
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_BOLD} --- Checking local disk space...${C_RESET} \n " ; fi
582
+ local required_space_kb=102400
565
583
local available_space_kb
566
584
available_space_kb=$( df --output=avail " $( dirname " ${LOG_FILE} " ) " | tail -n1)
567
585
if [[ " $available_space_kb " -lt " $required_space_kb " ]]; then
568
586
local err_msg=" Insufficient disk space in $( dirname " ${LOG_FILE} " ) to guarantee logging. ($(( available_space_kb / 1024 )) MB available)"
569
587
if [[ " $test_mode " == " true" ]]; then echo " ❌ FATAL: $err_msg " ; else send_notification " ❌ Backup FAILED: ${HOSTNAME} " " x" " ${NTFY_PRIORITY_FAILURE} " " failure" " FATAL: $err_msg " ; fi
570
588
exit 7
571
589
fi
572
- if [[ " $test_mode " == " true" ]]; then echo " ✅ Local disk space OK." ; fi
590
+ if [[ " $test_mode " == " true" ]]; then printf " ${C_GREEN} ✅ Local disk space OK.${C_RESET} \n " ; fi
573
591
fi
574
592
}
575
593
run_restore_mode () {
576
- echo " --- RESTORE MODE ACTIVATED ---"
594
+ printf " ${C_BOLD}${C_CYAN} --- RESTORE MODE ACTIVATED ---${C_RESET} \n "
577
595
run_preflight_checks " restore"
578
596
local DIRS_ARRAY; read -ra DIRS_ARRAY <<< " $BACKUP_DIRS"
579
597
local RECYCLE_OPTION=" [ Restore from Recycle Bin ]"
@@ -582,7 +600,7 @@ run_restore_mode() {
582
600
all_options+=(" $RECYCLE_OPTION " )
583
601
fi
584
602
all_options+=(" Cancel" )
585
- echo " Available backup sets to restore from:"
603
+ printf " ${C_YELLOW} Available backup sets to restore from:${C_RESET} \n "
586
604
select dir_choice in " ${all_options[@]} " ; do
587
605
if [[ -n " $dir_choice " ]]; then break ;
588
606
else echo " Invalid selection. Please try again." ; fi
@@ -593,27 +611,27 @@ run_restore_mode() {
593
611
local restore_path=" "
594
612
local is_full_directory_restore=false
595
613
if [[ " $dir_choice " == " $RECYCLE_OPTION " ]]; then
596
- echo " --- Browse Recycle Bin ---"
614
+ printf " ${C_BOLD}${C_CYAN} --- Browse Recycle Bin ---${C_RESET} \n "
597
615
local remote_recycle_path=" ${BOX_DIR%/ } /${RECYCLE_BIN_DIR%/ } "
598
616
local date_folders
599
617
date_folders=$( ssh " ${SSH_OPTS_ARRAY[@]} " " ${SSH_DIRECT_OPTS[@]} " " $BOX_ADDR " " ls -1 \" $remote_recycle_path \" " 2> /dev/null) || true
600
618
if [[ -z " $date_folders " ]]; then
601
619
echo " ❌ No dated folders found in the recycle bin. Nothing to restore." >&2
602
620
return 1
603
621
fi
604
- echo " Select a date to browse:"
622
+ printf " ${C_YELLOW} Select a backup run (date_time) to browse:${C_RESET} \n "
605
623
select date_choice in $date_folders " Cancel" ; do
606
624
if [[ " $date_choice " == " Cancel" ]]; then echo " Restore cancelled." ; return 0;
607
625
elif [[ -n " $date_choice " ]]; then break ;
608
626
else echo " Invalid selection. Please try again." ; fi
609
627
done
610
628
local remote_date_path=" ${remote_recycle_path} /${date_choice} "
611
- echo " --- Files available from ${date_choice} (showing first 20) ---"
629
+ printf " ${C_BOLD} --- Files available from ${date_choice} (showing first 20) ---${C_RESET} \n "
612
630
local remote_listing_source=" ${BOX_ADDR} :${remote_date_path} /"
613
631
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."
614
- echo " --------------------------------------------------------"
615
- local specific_path
616
- read -p " Enter the full original path of the item to restore (e.g., home/user/file.txt): " specific_path
632
+ printf " ${C_BOLD} --------------------------------------------------------${C_RESET} \n "
633
+ printf " ${C_YELLOW} Enter the full original path of the item to restore (e.g., home/user/file.txt): ${C_RESET} "
634
+ read -r specific_path
617
635
specific_path=$( echo " $specific_path " | sed ' s#^/##' )
618
636
if [[ -z " $specific_path " ]]; then echo " ❌ Path cannot be empty. Aborting." ; return 1; fi
619
637
full_remote_source=" ${BOX_ADDR} :${remote_date_path} /${specific_path} "
@@ -623,23 +641,24 @@ run_restore_mode() {
623
641
fi
624
642
default_local_dest=" /${specific_path} "
625
643
item_for_display=" (from Recycle Bin) '${specific_path} '"
626
- elif [[ " $dir_choice " == " Cancel" ]]; then
644
+ elif [[ " $dir_choice " == " Cancel" ]]; then
627
645
echo " Restore cancelled."
628
646
return 0
629
647
else
630
648
item_for_display=" the entire directory '${dir_choice} '"
631
649
while true ; do
632
- local choice_prompt= $' \n Restore the entire directory or a specific file/subfolder? [entire/specific]: '
633
- read -p " $choice_prompt " choice
650
+ printf " \n ${C_YELLOW} Restore the entire directory or a specific file/subfolder? [entire/specific]: ${C_RESET} "
651
+ read -r choice
634
652
case " $choice " in
635
653
entire)
636
654
is_full_directory_restore=true
637
655
break
638
656
;;
639
657
specific)
640
658
local specific_path_prompt
641
- printf -v specific_path_prompt " Enter the path relative to '%s' to restore: " " $dir_choice "
642
- read -ep " $specific_path_prompt " specific_path
659
+ printf -v specific_path_prompt " Enter the path relative to '%s' to restore: " " $dir_choice "
660
+ printf " ${C_YELLOW} %s${C_RESET} " " $specific_path_prompt "
661
+ read -er specific_path
643
662
specific_path=$( echo " $specific_path " | sed ' s#^/##' )
644
663
if [[ -n " $specific_path " ]]; then
645
664
restore_path=" $specific_path "
@@ -660,17 +679,16 @@ run_restore_mode() {
660
679
default_local_dest=$( echo " $dir_choice " | sed ' s#/\./#/#' )
661
680
fi
662
681
fi
663
- local final_dest
664
- local dest_prompt
665
- printf -v dest_prompt " \nEnter the destination path.\nPress [Enter] to use the original location (%s): " " $default_local_dest "
666
- read -p " $dest_prompt " final_dest
682
+ local final_dest
683
+ printf " \n${C_YELLOW} Enter the destination path.\n${C_DIM} Press [Enter] to use the original location (%s):${C_RESET} " " $default_local_dest "
684
+ read -r final_dest
667
685
: " ${final_dest:= $default_local_dest } "
668
686
local extra_rsync_opts=()
669
687
local dest_user=" "
670
688
if [[ " $final_dest " == /home/* ]]; then
671
689
dest_user=$( echo " $final_dest " | cut -d/ -f3)
672
690
if [[ -n " $dest_user " ]] && id -u " $dest_user " & > /dev/null; then
673
- echo " ℹ️ Home directory detected. Restored files will be owned by '${dest_user} '."
691
+ printf " ${C_CYAN} ℹ️ Home directory detected. Restored files will be owned by '${dest_user} '.${C_RESET} \n "
674
692
extra_rsync_opts+=(" --chown=${dest_user} :${dest_user} " )
675
693
else
676
694
dest_user=" "
@@ -696,34 +714,34 @@ run_restore_mode() {
696
714
if [[ " $dest_created " == " true" && " ${is_full_directory_restore:- false} " == " true" ]]; then
697
715
chmod 700 " $final_dest " ; log_message " Set permissions to 700 on newly created restore directory: $final_dest "
698
716
fi
699
- echo " Restore destination is set to: $final_dest "
700
- echo " " ; echo " --- PERFORMING DRY RUN. NO FILES WILL BE CHANGED. ---"
717
+ printf " Restore destination is set to: ${C_BOLD} %s ${C_RESET} \n " " $final_dest "
718
+ printf " \n ${C_BOLD}${C_YELLOW} --- PERFORMING DRY RUN. NO FILES WILL BE CHANGED. ---${C_RESET} \n "
701
719
log_message " Starting restore dry-run of ${item_for_display} from ${full_remote_source} to ${final_dest} "
702
720
local rsync_restore_opts=(-avhi --progress --exclude-from=" $EXCLUDE_FILE_TMP " -e " $SSH_CMD " )
703
721
if ! rsync " ${rsync_restore_opts[@]} " " ${extra_rsync_opts[@]} " --dry-run " $full_remote_source " " $final_dest " ; then
704
722
echo " ❌ DRY RUN FAILED. Rsync reported an error. Aborting." >&2 ; return 1
705
723
fi
706
- echo " --- DRY RUN COMPLETE ---"
724
+ printf " ${C_BOLD}${C_GREEN} --- DRY RUN COMPLETE ---${C_RESET} \n "
707
725
local confirmation
708
726
while true ; do
709
- local confirmation_prompt
710
- printf -v confirmation_prompt " \nAre you sure you want to proceed with restoring %s to '%s'? [yes/no]: " " $item_for_display " " $final_dest "
711
- read -p " $confirmation_prompt " confirmation
727
+ 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 "
728
+ read -r confirmation
729
+
712
730
case " $confirmation " in
713
731
yes) break ;;
714
732
no) echo " Restore aborted by user." ; return 0 ;;
715
733
* ) echo " Please answer yes or no." ;;
716
734
esac
717
735
done
718
- echo -e " \n--- PROCEEDING WITH RESTORE... ---"
736
+ printf " \n${C_BOLD} --- PROCEEDING WITH RESTORE... ---${C_RESET} \n "
719
737
log_message " Starting REAL restore of ${item_for_display} from ${full_remote_source} to ${final_dest} "
720
738
if rsync " ${rsync_restore_opts[@]} " " ${extra_rsync_opts[@]} " " $full_remote_source " " $final_dest " ; then
721
739
log_message " Restore completed successfully."
722
- echo " ✅ Restore of $item_for_display to '$final_dest ' completed successfully."
740
+ printf " ${C_GREEN} ✅ Restore of %s to '%s ' completed successfully.${C_RESET} \n " " $item_for_display " " $final_dest "
723
741
send_notification " ✅ Restore SUCCESS: ${HOSTNAME} " " white_check_mark" " ${NTFY_PRIORITY_SUCCESS} " " success" " Successfully restored ${item_for_display} to ${final_dest} "
724
742
else
725
743
log_message " Restore FAILED with rsync exit code $? ."
726
- echo " ❌ Restore FAILED. Check the rsync output and log for details."
744
+ printf " ${C_RED} ❌ Restore FAILED. Check the rsync output and log for details.${C_RESET} \n "
727
745
send_notification " ❌ Restore FAILED: ${HOSTNAME} " " x" " ${NTFY_PRIORITY_FAILURE} " " failure" " Restore of ${item_for_display} to ${final_dest} failed."
728
746
return 1
729
747
fi
0 commit comments