storageLabel="[Storage]" # Lists all backup directories on external storage with type labels (archive, daily, or unknown). function cmdStorageBackupList() { local dirList error dirCount archiveList dailyList run dirList error storageFileList "/" d || { printDanger "$error"; return 1; } dirCount=$(grep -c . <<< "$dirList" || true) archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true) printRow local i=0 num dir label while read -r dir; do ((++i)) num=$(printf "%0${#dirCount}d" "$i") label="$num: $fontBlue$dir$fontReset" if listContains "$dir" "$archiveList"; then printDotText "$label" "${fontGreen}archive$fontReset" elif listContains "$dir" "$dailyList"; then printDotText "$label" "${fontGreen}daily$fontReset" else printDotText "$label" "$labelUnknown" fi done < <(awk 'NF' <<< "$dirList") } # Rotates archive backups on external storage: deletes old entries exceeding $storageArchiveKeep. function cmdStorageBackupArchiveDispatch() { local dirList error run dirList error storageFileList "/" d || { printDanger "$error"; return 1; } printSection "Config" printDotText "Type" "$storageType" printDotText "Path" "$rsyncPath" printDotText "Archive keep" "$storageArchiveKeep" printDotText "Archive pattern" "$backupArchiveDirPattern" printSection "Directories found" archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) local archiveCount archiveRemoveCount i=0 num label dirDate dirDays archiveRemoveList="" archiveCount=$(grep -c . <<< "$archiveList" || true) if [[ "$archiveCount" -gt 0 ]]; then while read -r dir; do ((++i)) num=$(printf "%0${#archiveCount}d" "$i") label="$num: $fontBlue$dir$fontReset" if (( archiveCount - storageArchiveKeep >= i )); then printDotText "$label" "${fontRed}delete$fontReset" archiveRemoveList+=$'\n'"$dir" else printDotText "$label" "${fontGreen}save$fontReset" fi done < <(awk 'NF' <<< "$archiveList") archiveRemoveCount=$(grep -c . <<< "$archiveRemoveList" || true) if [[ "$archiveRemoveCount" -gt 0 ]]; then printSection "Deleting Directories" while read -r dir; do label="$dir" if ! runError error storageBackupRemove "$dir"; then printDotText "$label" "$labelFail" printDanger "$error" else printDotText "$label" "$labelDone" fi done < <(awk 'NF' <<< "$archiveRemoveList") fi fi } # Rotates daily backups on external storage: deletes old entries exceeding $storageDailyKeep, skips today. function cmdStorageBackupDailyDispatch() { local dirList error run dirList error storageFileList "/" d || { printDanger "$error"; return 1; } printSection "Config" printDotText "Type" "$storageType" printDotText "Path" "$rsyncPath" printDotText "Daily keep" "$storageDailyKeep" printDotText "Daily pattern" "$backupDailyDirPattern" printSection "Directories found" dailyList=$(printf '%s\n' "$dirList" | grep -v "$appDate" | grep -E -- "$backupDailyDirPattern" | sort || true) local dailyCount dailyRemoveCount i=0 num label dailyRemoveList="" dailyCount=$(grep -c . <<< "$dailyList" || true) if [[ "$dailyCount" -gt 0 ]]; then while read -r dir; do ((++i)) num=$(printf "%0${#dailyCount}d" "$i") label="$num: $fontBlue$dir$fontReset" if [[ "$dir" == "$appDate" ]]; then printDotText "$label" "${fontGray}skipped$fontReset" elif (( dailyCount - storageDailyKeep >= i )); then printDotText "$label" "${fontRed}delete$fontReset" dailyRemoveList+=$'\n'"$dir" else printDotText "$label" "${fontGreen}save$fontReset" fi done < <(awk 'NF' <<< "$dailyList") dailyRemoveCount=$(grep -c . <<< "$dailyRemoveList" || true) if [[ "$dailyRemoveCount" -gt 0 ]]; then printSection "Deleting Directories" while read -r dir; do label="$dir" if ! runError error storageBackupRemove "$dir"; then printDotText "$label" "$labelFail" printDanger "$error" else printDotText "$label" "$labelDone" fi done < <(awk 'NF' <<< "$dailyRemoveList") fi fi } # Synchronizes the local path to external storage (rsync or SSH). # $1 (sourcePath): local path to sync. # [$2] (type): storage type (rsync or ssh); defaults to $storageType. function cmdStoragePathSync() { local sourcePath="$1" [[ -n "$sourcePath" ]] || { printDanger "Source path not specified"; return 1; } local type="${2:-$storageType}" local error printSection "Config" printDotText "Source" "$sourcePath" if [[ ! -e "$sourcePath" ]]; then printDotText "$sourcePath" "${fontRed}not found$fontReset" return 1 fi case "$type" in rsync) printDotText "Type" "$type" printDotText "rSync URI" "$rsyncUri" printDotText "rSync path" "$rsyncPath" printSection "Executing" runError error storagePathSyncRsync "$sourcePath" || { printDotText "synchronization" "$labelFail" printDanger "$error" return 1 } ;; ssh) printDotText "Type" "$type" printDotText "SSH URI" "$sshUser@$sshHost" printDotText "SSH path" "$sshPath" printSection "Executing" runError error storagePathSyncSSH "$sourcePath" || { printDotText "synchronization" "$labelFail" printDanger "$error" return 1 } ;; *) printDotText "Type" "$type $labelUnknown" return 1 ;; esac printDotText "synchronization" "$labelDone" } # Syncs today's daily backup ($backupDailyPath/$appDate) to external storage. function cmdStorageBackupDailySyncLast() { cmdStoragePathSync "$backupDailyPath/$appDate" } # Syncs the newest archive backup directory to external storage. function cmdStorageBackupArchiveSyncLast() { local dirList archiveList archiveDir error run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; } archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) archiveDir=$(tail -n 1 <<< "$archiveList") [[ -n "$archiveDir" ]] || { printDanger "No archive directories found"; return 1; } cmdStoragePathSync "$backupArchivePath/$archiveDir" } # Interactively selects a local archive backup directory and syncs it to external storage. function cmdStorageBackupArchiveSync() { local dirList error archiveList archiveCount run dirList error fileList "$backupArchivePath" d || { printDanger "$error"; return 1; } archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) archiveCount=$(grep -c . <<< "$archiveList" || true) printRow local i=0 num dir while read -r dir; do ((++i)) num=$(printf "%0${#archiveCount}d" "$i") printDotText "$num: $fontBlue$dir$fontReset" "${fontGreen}archive$fontReset" done < <(awk 'NF' <<< "$archiveList") printRow local input item selected read -r -p "Specify the backup number: " input printRow [[ -z "$input" ]] && input=0 [[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; } item=$((10#$input)) selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$archiveList") [[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; } cmdStoragePathSync "$backupArchivePath/$selected" } # Interactively selects a local daily backup directory and syncs it to external storage. function cmdStorageBackupDailySync() { local dirList error dailyList dailyCount run dirList error fileList "$backupDailyPath" d || { printDanger "$error"; return 1; } dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true) dailyCount=$(grep -c . <<< "$dailyList" || true) printRow local i=0 num dir while read -r dir; do ((++i)) num=$(printf "%0${#dailyCount}d" "$i") printDotText "$num: $fontBlue$dir$fontReset" "${fontGreen}daily$fontReset" done < <(awk 'NF' <<< "$dailyList") printRow local input item selected read -r -p "Specify the backup number: " input printRow [[ -z "$input" ]] && input=0 [[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; } item=$((10#$input)) selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$dailyList") [[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; } cmdStoragePathSync "$backupDailyPath/$selected" } # Dispatches interactive backup sync by type. # $1 (type): backup type: archive or daily. function cmdStorageBackupSync() { local type type="$1" case "$type" in archive) cmdStorageBackupArchiveSync ;; daily) cmdStorageBackupDailySync ;; *) printDanger "Unknown type backup: $type"; return 1 ;; esac } # Interactively selects a backup directory on external storage and removes it. function cmdStorageBackupRemove() { local dirList error dirCount archiveList dailyList dir i=0 num label run dirList error storageFileList "/" d || { printDanger "$error"; return 1; } dirCount=$(grep -c . <<< "$dirList" || true) archiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) dailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true) printRow while read -r dir; do ((++i)) num=$(printf "%0${#dirCount}d" "$i") label="$num: $fontBlue$dir$fontReset" if listContains "$dir" "$archiveList"; then printDotText "$label" "${fontGreen}archive$fontReset" elif listContains "$dir" "$dailyList"; then printDotText "$label" "${fontGreen}daily$fontReset" else printDotText "$label" "$labelUnknown" fi done < <(awk 'NF' <<< "$dirList") printRow local input item selected read -r -p "Specify the backup number: " input printRow [[ -z "$input" ]] && input=0 [[ "$input" =~ ^[0-9]+$ ]] || { printDanger "Invalid backup number"; return 1; } item=$((10#$input)) selected=$(awk 'NF {n++} n == item {print; exit}' item="$item" <<< "$dirList") [[ -n "$selected" ]] || { printDanger "Unknown backup number"; return 1; } if ! runError error storageBackupRemove "$selected"; then printDotText "$selected" "$labelFail" printDanger "$error" return 1 fi printDotText "$selected" "$labelDone" } # Compares local and remote backup directories, showing which exist on each side. function cmdStorageBackupCompare() { local dirList error localArchiveList localDailyList remoteArchiveList remoteDailyList run dirList error fileList "$backupArchivePath" d || { printDanger "Archive path: $error"; return 1; } localArchiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) run dirList error fileList "$backupDailyPath" d || { printDanger "Archive path: $error"; return 1; } localDailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true) run dirList error storageFileList "/" d || { printDanger "$error"; return 1; } remoteArchiveList=$(printf '%s\n' "$dirList" | grep -E -- "$backupArchiveDirPattern" | sort || true) remoteDailyList=$(printf '%s\n' "$dirList" | grep -E -- "$backupDailyDirPattern" | sort || true) printSection "Local" printDotText "Archive" "$(grep -c . <<< "$localArchiveList" || true)" printDotText "Daily" "$(grep -c . <<< "$localDailyList" || true)" printSection "Remote" printDotText "Archive" "$(grep -c . <<< "$remoteArchiveList" || true)" printDotText "Daily" "$(grep -c . <<< "$remoteDailyList" || true)" local allDirs allDirs=$(printf '%s\n' "$localArchiveList" "$localDailyList" "$remoteArchiveList" "$remoteDailyList" | awk 'NF' | sort -u) printSection "Comparison" local dir localStatus remoteStatus out="${fontRed}X$fontReset" localIn="${fontGreen}L$fontReset" remoteIn="${fontGreen}R$fontReset" while read -r dir; do localStatus="$out" remoteStatus="$out" if listContains "$dir" "$localArchiveList"; then localStatus="$localIn" elif listContains "$dir" "$localDailyList"; then localStatus="$localIn" fi if listContains "$dir" "$remoteArchiveList"; then remoteStatus="$remoteIn" elif listContains "$dir" "$remoteDailyList"; then remoteStatus="$remoteIn" fi printDotText "$fontBlue$dir$fontReset" "[ $localStatus : $remoteStatus ]" done < <(awk 'NF' <<< "$allDirs") }