#!/bin/bash { #//////////////////////////////////// # DietPi Sync # #//////////////////////////////////// # Created by Daniel Knight / daniel.knight@dietpi.com / dietpi.com # #//////////////////////////////////// # # Info: # - Location: /boot/dietpi/dietpi-sync # - Allows user to sync a Source and Target directory. # # Usage: # - /boot/dietpi/dietpi-sync = Menu # - /boot/dietpi/dietpi-sync 1 = Sync #//////////////////////////////////// # Import DietPi-Globals -------------------------------------------------------------- . /boot/dietpi/func/dietpi-globals readonly G_PROGRAM_NAME='DietPi-Sync' G_CHECK_ROOT_USER "$@" G_CHECK_ROOTFS_RW G_INIT # Import DietPi-Globals -------------------------------------------------------------- # Restart rsync service, if stopped during DietPi-Sync run: https://github.com/MichaIng/DietPi/issues/1869 SERVICE_CONTROL=0 # shellcheck disable=SC2329 G_EXIT_CUSTOM(){ (( $SERVICE_CONTROL )) && G_EXEC systemctl start rsync; } # Grab input (valid interger) disable_error=1 G_CHECK_VALIDINT "$1" && INPUT=$1 || INPUT=0 #///////////////////////////////////////////////////////////////////////////////////// # Sync System #///////////////////////////////////////////////////////////////////////////////////// EXIT_CODE=0 # File paths readonly FP_SETTINGS='/boot/dietpi/.dietpi-sync_settings' readonly FP_FILTER_INCLUDE_EXCLUDE='.dietpi-sync_include_exclude' readonly FP_USER_FILTER_INCLUDE_EXCLUDE='/boot/dietpi/.dietpi-sync_inc_exc' readonly FP_LOG='.dietpi-sync.log' readonly FP_LOG_ALT='/var/tmp/dietpi/logs/dietpi-sync.log' FP_SOURCE='/mnt/source' FP_TARGET='/mnt/target' # Extra settings SYNC_DELETE_MODE=0 SYNC_CRONDAILY=0 Create_Filter_Include_Exclude(){ # Exclude files by name cat << _EOF_ > "$FP_FILTER_INCLUDE_EXCLUDE" - $FP_LOG - $FP_FILTER_INCLUDE_EXCLUDE - .swap* - *.tmp # - MS Windows specific - Thumbs.db - desktop.ini - SyncToy* # MS SyncToy - System Volume Information # causes error code 23 (permission denied) _EOF_ # Exclude specific files/dirs by path local aexclude=( "$FP_TARGET" "$FP_SETTINGS" "$FP_USER_FILTER_INCLUDE_EXCLUDE" '/var/swap' ) for i in "${aexclude[@]}" do # Exclude only, if inside source location, via path, relative to source location [[ $i == "$FP_SOURCE/"* ]] && echo "- $(realpath --relative-to="$FP_SOURCE" "$i")" >> "$FP_FILTER_INCLUDE_EXCLUDE" done unset aexclude # Add users additional list [[ -f $FP_USER_FILTER_INCLUDE_EXCLUDE ]] && cat "$FP_USER_FILTER_INCLUDE_EXCLUDE" >> "$FP_FILTER_INCLUDE_EXCLUDE" } Run_Sync(){ # Stop rsync service: https://github.com/MichaIng/DietPi/issues/1869 if systemctl -q is-active rsync; then SERVICE_CONTROL=1 G_EXEC systemctl stop rsync fi # Pre-create target dir, which also checks R/W access # - Remove previous mkdir log [[ -f $FP_LOG_ALT ]] && rm "$FP_LOG_ALT" mkdir -p "$FP_TARGET" &> "$FP_LOG" # Error: Dir not found if [[ ! -d $FP_TARGET ]]; then G_WHIP_MSG "[FAILED] Unable to pre-create target directory: $FP_TARGET\n\n\"mkdir\" reported the following error:\n$(<"$FP_LOG")" # We cannot log to target dir, use $FP_LOG_ALT instead echo "$(date +"%Y-%m-%d_%T") [FAILED] Unable to pre-create target directory: $FP_TARGET" >> "$FP_LOG" mv "$FP_LOG" "$FP_LOG_ALT" # Error: Empty source dir elif [[ ! $(ls -A "$FP_SOURCE") ]]; then G_WHIP_MSG "[FAILED] The chosen source directory is empty: $FP_SOURCE\n\nPlease check its path and in case verify that its mount is active." # Log to target dir directly, preserve previous rsync log echo "$(date +"%Y-%m-%d_%T") [FAILED] The chosen source directory is empty: $FP_SOURCE" >> "$FP_TARGET/$FP_LOG" # Error: Rsync still running elif pgrep -x 'rsync' &> /dev/null; then G_WHIP_MSG '[FAILED] Rsync is already running and failed to stop gracefully\n\nPlease check active rsync processes e.g. via "htop" and finish or kill them.' # Log to target dir directly, preserve previous rsync log echo "$(date +"%Y-%m-%d_%T") [FAILED] Rsync is already running and failed to stop gracefully" >> "$FP_TARGET/$FP_LOG" # Start sync, beginning with dry run else # Generate Exclude/Include lists Create_Filter_Include_Exclude # Rsync options local aoptions=('-aHP4' '--info=name0' '--info=progress2' "--exclude-from=$FP_FILTER_INCLUDE_EXCLUDE" "--log-file=$FP_LOG") # - Delete mode? (( $SYNC_DELETE_MODE )) && aoptions+=('--delete' '--delete-excluded') # Dry run G_DIETPI-NOTIFY 3 "$G_PROGRAM_NAME" "Dry run $FP_SOURCE > $FP_TARGET" rsync --dry-run --stats "${aoptions[@]}" "$FP_SOURCE/" "$FP_TARGET/" > .dietpi-sync_result EXIT_CODE=$? if (( ! $EXIT_CODE )); then # Free space check # - NB: Working in KiB until end MiB conversion, as, don't like using long long int, and, KiB should offer a good end result. local old_backup_size=$(du -ks "$FP_TARGET" | mawk '{print $1}') # Actual disk space usage local new_backup_size=$(( $(grep -m1 '^Total file size:' .dietpi-sync_result | sed 's/[^0-9]*//g') / 1024 + 1 )) # Theoretical, data size only # - NB: Number of files $4 includes files + dirs + links. # Actually, we don't want to count links, as they don't use block sizes. # But scraping files + dirs is too much effort, as the output format depends on their existence. local total_file_count=$(mawk '/^Number of files:/ {print $4;exit}' .dietpi-sync_result | sed 's/[^0-9]*//g') local target_fs_blocksize=$(stat -fc %s "$FP_TARGET") new_backup_size=$(( $new_backup_size + $total_file_count * $target_fs_blocksize / 1024 + 1 )) # Add one block size to each file as worst case result local end_result=$(( ( $new_backup_size - $old_backup_size ) / 1024 + 1 )) local menu_test="[ OK ] Dry run completed (NO changes made):\n - $FP_SOURCE > $FP_TARGET" G_WHIP_DEFAULT_ITEM='Sync' if ! G_CHECK_FREESPACE "$FP_TARGET" "$end_result"; then if (( $INPUT == 1 )); then echo "$(date +"%Y-%m-%d_%T") [FAILED] Insufficient free space" >> "$FP_TARGET/$FP_LOG" return 1 else menu_test="[WARNING] Insufficient free space\n - $FP_SOURCE > $FP_TARGET\n The target location appears to have insufficient free space to successfully finish the sync. However, this check is a rough estimate in reasonable time and may be slightly incorrect.\n Continue only if you know what you are doing and after checking the log!" G_WHIP_DEFAULT_ITEM='Cancel' fi fi [[ $INPUT == 1 ]] || while : do G_WHIP_MENU_ARRAY=( 'Sync' ': Continue with real sync' 'Log' ': View log file for details' 'Cancel' ': Abort sync, no changes will be made' ) G_WHIP_MENU "$menu_test\n\n$(sed -n 4,9p .dietpi-sync_result)" case $G_WHIP_RETURNED_VALUE in 'Log') G_WHIP_VIEWFILE "$FP_LOG"; G_WHIP_DEFAULT_ITEM='Log';; 'Sync') break;; *) return;; esac done # Sync G_DIETPI-NOTIFY 3 "$G_PROGRAM_NAME" "Sync $FP_SOURCE > $FP_TARGET" # - Clear log file from dry run > "$FP_LOG" rsync "${aoptions[@]}" "$FP_SOURCE/" "$FP_TARGET/" EXIT_CODE=$? fi G_DIETPI-NOTIFY -1 "$EXIT_CODE" "$G_PROGRAM_NAME" if (( $EXIT_CODE )); then echo "$(date +"%Y-%m-%d_%T") [FAILED] Please see the log file for more information: $FP_TARGET/$FP_LOG" >> "$FP_LOG" G_WHIP_MSG "[FAILED] $FP_SOURCE > $FP_TARGET\n\nYou will given an option to view the logfile on the next screen. Please check it for information and/or errors." else echo "$(date +"%Y-%m-%d_%T") [ OK ] Sync completed" >> "$FP_LOG" G_WHIP_MSG "[ OK ] Sync completed:\n - $FP_SOURCE > $FP_TARGET" fi log=1 G_WHIP_VIEWFILE "$FP_LOG" mv "$FP_LOG" "$FP_TARGET/" fi } Check_Available_DietPi_Mounts(){ local samba_mount_out=$(df -h | grep "$fp_samba_mount") if [[ $samba_mount_out ]]; then samba_mount_available=1 samba_mount_text=$(mawk '{print "Size: "$2"iB | Available: "$4"iB"}' <<< "$samba_mount_out") fi } #///////////////////////////////////////////////////////////////////////////////////// # Settings File #///////////////////////////////////////////////////////////////////////////////////// Write_Settings_File(){ cat << _EOF_ > "$FP_SETTINGS" FP_SOURCE='$FP_SOURCE' FP_TARGET='$FP_TARGET' SYNC_DELETE_MODE=$SYNC_DELETE_MODE SYNC_CRONDAILY=$SYNC_CRONDAILY _EOF_ } # shellcheck disable=SC1090 Read_Settings_File(){ [[ -f $FP_SETTINGS ]] && . "$FP_SETTINGS"; } #///////////////////////////////////////////////////////////////////////////////////// # MENUS #///////////////////////////////////////////////////////////////////////////////////// TARGETMENUID=0 SYNC_MODE_TEXT= SYNC_CRONDAILY_TEXT= # TARGETMENUID=0 MENU_LASTITEM_MAIN= Menu_Main(){ (( $SYNC_DELETE_MODE )) && SYNC_MODE_TEXT='On' || SYNC_MODE_TEXT='Off' (( $SYNC_CRONDAILY )) && SYNC_CRONDAILY_TEXT='On' || SYNC_CRONDAILY_TEXT='Off' if [[ -f $FP_TARGET/$FP_LOG ]]; then local sync_last_status=$(tail -1 "$FP_TARGET/$FP_LOG") elif [[ -f $FP_LOG_ALT ]]; then local sync_last_status=$(tail -1 "$FP_LOG_ALT") else local sync_last_status='No previous sync found in target directory.' fi G_WHIP_MENU_ARRAY=( '' '●─ Info ' 'Help' ": What does $G_PROGRAM_NAME do?" '' '●─ Options ' 'Source Location' ": [$FP_SOURCE] Sync FROM here" 'Target Location' ": [$FP_TARGET] Sync TO here" 'Delete Mode' ": [$SYNC_MODE_TEXT] Sync file removals" 'Daily Sync' ": [$SYNC_CRONDAILY_TEXT] Daily sync via cron job" '' '●─ Run ' 'Dry run + Sync' ': Sync from Source to Target (Dry run included!)' ) G_WHIP_DEFAULT_ITEM=$MENU_LASTITEM_MAIN G_WHIP_BUTTON_CANCEL_TEXT='Exit' if G_WHIP_MENU "Last sync status:\n $sync_last_status"; then MENU_LASTITEM_MAIN=$G_WHIP_RETURNED_VALUE case "$G_WHIP_RETURNED_VALUE" in 'Help') G_WHIP_MSG 'DietPi-Sync is a program that allows you to duplicate a directory from one location (Source) to another (Target).\n For example: If we want to duplicate (sync) the data on your external USB HDD to another location, we simply select the USB HDD as the source, then, select a target location. The target location can be anything from a networked samba fileserver, or even an FTP server.\n Each sync includes a leading dry run, after which you can check the expected result before deciding if you want to continue with the actual sync.\n More information:\n - https://dietpi.com/docs/dietpi_tools/#dietpi-sync';; 'Source Location') TARGETMENUID=2;; 'Target Location') TARGETMENUID=1;; 'Delete Mode') Menu_Set_Sync_Delete_Mode;; 'Daily Sync') Menu_Set_CronDaily;; 'Dry run + Sync') Run_Sync;; *) :;; esac else Menu_Exit fi } Menu_Exit(){ G_WHIP_SIZE_X_MAX=50 G_WHIP_YESNO "Exit $G_PROGRAM_NAME?" && TARGETMENUID=-1 } # # TARGETMENUID: 2=Source | 1=Target MENU_LASTITEM_SET_DIRS= Menu_Set_Directories(){ local current_directory=$FP_TARGET local current_mode_text='Target' local whip_description_text="Please select the $current_mode_text location.\nA copy of all the files and folders in the Source location will be created here.\n\nCurrent $current_mode_text location:\n$current_directory" if (( $TARGETMENUID == 2 )); then current_directory=$FP_SOURCE current_mode_text='Source' whip_description_text="Please select the $current_mode_text location.\nA copy of all the files and folder in this Source location, will be created at the Target location.\n\nCurrent $current_mode_text location:\n$current_directory" fi # Check for samba mount local fp_samba_mount='/mnt/samba' local samba_mount_available=0 local samba_mount_text="Not mounted ($fp_samba_mount). Select to setup." Check_Available_DietPi_Mounts G_WHIP_MENU_ARRAY=( 'Manual' ": Manually type your $current_mode_text directory." 'List' ': Select from a list of available mounts/drives' 'Samba Client' ": [$samba_mount_text]" ) G_WHIP_DEFAULT_ITEM=$MENU_LASTITEM_SET_DIRS G_WHIP_BUTTON_CANCEL_TEXT='Back' if G_WHIP_MENU "$whip_description_text"; then MENU_LASTITEM_SET_DIRS=$G_WHIP_RETURNED_VALUE case "$G_WHIP_RETURNED_VALUE" in 'List') if /boot/dietpi/dietpi-drive_manager 1; then local return_value=$( Exact copy' ) G_WHIP_DEFAULT_ITEM=$SYNC_MODE_TEXT if G_WHIP_MENU 'Please select the Sync delete mode.\n Off: (safe)\nIf files and folders exist in the Target location, that are not in the Source, they will be left alone.\n On: (WARNING, if in doubt, DO NOT enable)\nAn exact copy of the Source location will be created at the Target location. If files are in the Target location that do not exist in the Source, they will be DELETED.'; then [[ $G_WHIP_RETURNED_VALUE == 'On' ]] && SYNC_DELETE_MODE=1 || SYNC_DELETE_MODE=0 fi } Menu_Set_CronDaily(){ G_WHIP_MENU_ARRAY=( 'Off' ': Manual sync only' 'On' ': Automatically sync once a day' ) G_WHIP_DEFAULT_ITEM=$SYNC_CRONDAILY_TEXT if G_WHIP_MENU 'Off:\nThe user must manually sync using dietpi-sync. \nOn:\nA cron job will automatically run dietpi-sync, once a day. \n(NOTICE):\nWhen using this feature, please run a manual sync and check the dry run log to verify what will happen.'; then [[ $G_WHIP_RETURNED_VALUE == 'On' ]] && SYNC_CRONDAILY=1 || SYNC_CRONDAILY=0 fi } Input_User_Directory(){ # TARGETMENUID: 2=Source | 1=Target local mode='target' (( $TARGETMENUID == 2 )) && mode='source' local fp_mode="FP_${mode^^}" G_WHIP_DEFAULT_ITEM=${!fp_mode} local text="Please enter the full path to the desired $mode directory.\n\n - eg: /mnt/$mode" while : do if G_WHIP_INPUTBOX "$text"; then # Sanity checks # - Full path required if [[ $G_WHIP_RETURNED_VALUE != /* ]]; then text="ERROR: Please enter the $mode location as full path with leading slash: /\n\n - eg: /mnt/$mode" # - Do not allow to sync from or to root elif [[ $G_WHIP_RETURNED_VALUE == / ]]; then text="ERROR: You entered the root directory as $mode location: /\n\n - Please use \"dietpi-backup\" instead for system backups and recoveries." else declare -g "$fp_mode=${G_WHIP_RETURNED_VALUE%/}" TARGETMENUID=0 return fi else return fi G_WHIP_DEFAULT_ITEM=$G_WHIP_RETURNED_VALUE done } Prompt_Setup_Samba_Mount(){ G_WHIP_YESNO "$samba_mount_text\n\nWould you like to run DietPi-Drive_Manager and setup your Samba Client Mount now?" && /boot/dietpi/dietpi-drive_manager; } #///////////////////////////////////////////////////////////////////////////////////// # Main Loop #///////////////////////////////////////////////////////////////////////////////////// # Pre-reqs, install if required G_AG_CHECK_INSTALL_PREREQ rsync # Generate optional user include/exclude file if [[ ! -f $FP_USER_FILTER_INCLUDE_EXCLUDE ]]; then cat << _EOF_ > "$FP_USER_FILTER_INCLUDE_EXCLUDE" #$G_PROGRAM_NAME | Custom include/exclude filters # #To EXCLUDE (-) all files by name: #- file #To EXCLUDE (-) a single file by path, relative to source directory: #- /relative/path/to/file #To EXCLUDE (-) a specific directory by path, relative to source directory: #- /relative/path/to/directory/ # #To INCLUDE (+) something from within an otherwise excluded directory: #+ /relative/path/to/directory/include_this _EOF_ fi Read_Settings_File #----------------------------------------------------------------------------- # Run sync if (( $INPUT == 1 )); then Run_Sync #----------------------------------------------------------------------------- # Run menu elif (( $INPUT == 0 )); then until (( $TARGETMENUID < 0 )) do G_TERM_CLEAR if [[ $TARGETMENUID == [12] ]]; then Menu_Set_Directories else Menu_Main fi done Write_Settings_File fi #----------------------------------------------------------------------------------- exit "$EXIT_CODE" #----------------------------------------------------------------------------------- }