├── .gitignore ├── config.sh ├── functions.sh ├── readme.md └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | .git* 2 | todo.txt -------------------------------------------------------------------------------- /config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ################################################################# 4 | # A bash script to setup automated backups for your WordPress websites using rclone and wp-cli 5 | # By: Ali Khallad 6 | # URL: https://alikhallad.com | https://wpali.com 7 | # Tested on: Ubuntu 22.04 8 | # Tested with: rclone v1.53.3, WP-CLI 2.6.0 9 | ################################################################# 10 | 11 | #################################################################################################### 12 | ############################## LOAD FUNCTIONS & VARIABLE DEFINITIONS ############################### 13 | #################################################################################################### 14 | 15 | # Define main paths 16 | CRON_SCRIPTS_DIR="cron_scripts" 17 | TMP_DIR="$PWD/tmp" 18 | DEFINITIONS_FILE="definitions" 19 | LOG_FILE="$PWD/backup.log" 20 | CRON_FILE="/etc/cron.d/rclone-automated-backups-by-alikhallad" 21 | # Define ANSI color codes 22 | BOLD="\033[1m" 23 | UNDERLINE="\033[4m" 24 | RED="\e[31m" 25 | RED_BG="\e[41m" 26 | GREEN="\e[32m" 27 | GREEN_BG="\e[42m" 28 | YELLOW="\e[33m" 29 | BLUE="\e[34m" 30 | BLUE_BG="\e[44m" 31 | RESET="\e[0m" # Reset text formatting 32 | 33 | # Load the functions file 34 | source functions.sh 35 | 36 | # Clear screen 37 | clear_screen 38 | 39 | # Check if the definitions file exists 40 | if [ -f "$DEFINITIONS_FILE" ]; then 41 | # Load the definitions file 42 | source "$DEFINITIONS_FILE" 43 | # Update definitions state variables 44 | update_definitions_state 45 | 46 | # Define an array of required variables 47 | required_vars=("DOMAINS" "PATHS") 48 | 49 | # Check if all required variables are defined, otherwise update the definitions 50 | for var in "${required_vars[@]}"; do 51 | # Use -v to check if the variable is defined 52 | if ! declare -p "$var" &>/dev/null; then 53 | echo -e "${YELLOW}####################################################################################################${RESET}" 54 | echo -e "${YELLOW}# The definitions file is missing some required variables. A fresh copy has been generated.${RESET}" 55 | echo -e "${YELLOW}# - Previous configurations will be lost.${RESET}" 56 | echo -e "${YELLOW}# - Previosuly automated backups should continue to work as usual.${RESET}" 57 | echo -e "${YELLOW}####################################################################################################${RESET}" 58 | # Regenerate the file content and load it again 59 | update_definitions 60 | source "$DEFINITIONS_FILE" 61 | break 62 | fi 63 | done 64 | 65 | else 66 | # If the file is missing, create it 67 | sudo touch "$DEFINITIONS_FILE" 68 | # Regenerate the file content and load it again 69 | update_definitions 70 | source "$DEFINITIONS_FILE" 71 | fi 72 | 73 | #################################################################################################### 74 | ############################## AUTOMATED CHECKS TO VERIFY SYSTEM SETUP ############################# 75 | #################################################################################################### 76 | 77 | # Check if the user has sudo privileges 78 | if sudo -n true 2>/dev/null; then 79 | echo -e "${GREEN}1. Current user has sudo privileges.${RESET}" 80 | else 81 | echo -e "${RED}1. Current user does not have sudo privileges. This script is only available for sudo users.${RESET}" 82 | echo "" 83 | exit 1 84 | fi 85 | 86 | # Check if wp cli is available 87 | if command -v wp &>/dev/null; then 88 | echo -e "${GREEN}2. wp cli is available.${RESET}" 89 | elif [ -f "/usr/local/bin/wp" ]; then 90 | echo -e "${YELLOW}2. wp cli found in /usr/local/bin/wp. To make it available system-wide:${RESET}" 91 | echo -e "${YELLOW}Run: ${RESET}${BOLD}${YELLOW}sudo ln -s /usr/local/bin/wp /usr/bin/wp${RESET}" 92 | echo "" 93 | exit 1 94 | else 95 | echo -e "${RED}2. wp cli is not available. Please install it before running the script.${RESET}" 96 | echo -e "${RED}To install wp-cli, follow this guide:${RESET}" 97 | echo -e "${RED}https://wp-cli.org/#installing${RESET}" 98 | echo "" 99 | exit 1 100 | fi 101 | 102 | # Check if rclone is available 103 | if command -v rclone &>/dev/null; then 104 | echo -e "${GREEN}3. rclone is available.${RESET}" 105 | elif [ -f "/usr/local/bin/rclone" ]; then 106 | echo -e "${YELLOW}3. rclone found in /usr/local/bin/rclone. To make it available system-wide:${RESET}" 107 | echo -e "${YELLOW}Run: ${RESET}${BOLD}${YELLOW}sudo ln -s /usr/local/bin/rclone /usr/bin/rclone${RESET}" 108 | echo "" 109 | exit 1 110 | else 111 | echo -e "${RED}3. rclone is not available. Please install it before running the script.${RESET}" 112 | echo -e "${RED}To install rclone, follow this guide:${RESET}" 113 | echo -e "${RED}https://rclone.org/install/${RESET}" 114 | echo "" 115 | exit 1 116 | fi 117 | 118 | # Check if restic is available 119 | if command -v restic &>/dev/null; then 120 | echo -e "${GREEN}4. restic is available.${RESET}" 121 | RESTIC_AVAILABLE=true 122 | elif [ -f "/usr/local/bin/restic" ]; then 123 | echo -e "${YELLOW}4. restic found in /usr/local/bin/restic. To make it available system-wide:${RESET}" 124 | echo -e "${YELLOW}Run: ${RESET}${BOLD}${YELLOW}sudo ln -s /usr/local/bin/restic /usr/bin/restic${RESET}" 125 | RESTIC_AVAILABLE=false 126 | else 127 | echo -e "${YELLOW}4. restic is not available ( optional for incremental backups ).${RESET}" 128 | RESTIC_AVAILABLE=false 129 | fi 130 | 131 | # Check if automated backups are configured correctly 132 | if [ $HAS_AUTOMATED_BACKUPS == true ]; then 133 | 134 | echo -e "${GREEN}5. Automated backups has been configured.${RESET}" 135 | 136 | echo "" 137 | echo -e "${GREEN_BG}---------------------------------------------------------------------------${RESET}" 138 | echo -e "${GREEN_BG}-------------------- ALL CHECKS COMPLETED SUCCESSFULLY --------------------${RESET}" 139 | echo -e "${GREEN_BG}------------------------ MANAGE YOUR BACKUPS BELOW ------------------------${RESET}" 140 | echo -e "${GREEN_BG}---------------------------------------------------------------------------${RESET}" 141 | 142 | else 143 | echo -e "${YELLOW}5. Automated backups has not been configured.${RESET}" 144 | 145 | echo "" 146 | echo -e "${GREEN_BG}---------------------------------------------------------------------------${RESET}" 147 | echo -e "${GREEN_BG}------------------- SYSTEM CHECKS COMPLETED SUCCESSFULLY ------------------${RESET}" 148 | echo -e "${GREEN_BG}------------------ CONFIGURE YOUR AUTOMATED BACKUPS BELOW -----------------${RESET}" 149 | echo -e "${GREEN_BG}---------------------------------------------------------------------------${RESET}" 150 | 151 | fi 152 | 153 | #################################################################################################### 154 | ################################# OUTPUT THE CONFIGURATION OPTIONS ################################# 155 | #################################################################################################### 156 | 157 | while true; do 158 | # Reset the "clear_screen_last_caller_name" function each time the main menu is generated: 159 | clear_screen_last_caller_name="" 160 | 161 | echo "" 162 | ########################################################## 163 | ########################## 1. Q ########################## 164 | ########################################################## 165 | if [[ $ARE_DOMAINS_EMPTY == true && $ARE_DOMAINS_EMPTY != -1 ]]; then 166 | echo -e "${BLUE_BG}${BOLD}################# MAIN MENU ################${RESET}" 167 | echo -e "${BLUE_BG}${BOLD}############# Add a site/domain ############${RESET}" 168 | 169 | echo -e "${BOLD}1. Add a site/domain${RESET}" 170 | echo "2. Quit" 171 | ########################################################## 172 | ########################## 2. Q ########################## 173 | ########################################################## 174 | elif [ $IS_RCLONE_CONFIGURED == false ]; then 175 | echo -e "${BLUE_BG}${BOLD}################# MAIN MENU ################${RESET}" 176 | echo -e "${BLUE_BG}${BOLD}######### Configure rclone remotes #########${RESET}" 177 | 178 | if [ $ARE_DOMAINS_EMPTY == -1 ]; then 179 | echo "1. Add a site/domain" 180 | else 181 | echo "1. Manage sites/domains" 182 | fi 183 | 184 | echo -e "${BOLD}2. Configure rclone (remotes)${RESET}" 185 | echo "3. Quit" 186 | ########################################################## 187 | ########################## 3. Q ########################## 188 | ########################################################## 189 | elif [ $HAS_AUTOMATED_BACKUPS == false ]; then 190 | echo -e "${BLUE_BG}${BOLD}################# MAIN MENU ################${RESET}" 191 | echo -e "${BLUE_BG}${BOLD}######### Create an automated backup #######${RESET}" 192 | 193 | if [ $ARE_DOMAINS_EMPTY == -1 ]; then 194 | echo "1. Add a site/domain" 195 | else 196 | echo "1. Manage sites/domains" 197 | fi 198 | 199 | echo "2. Re-configure rclone (remotes)" 200 | echo -e "${BOLD}3. Create an automated backup${RESET}" 201 | echo "4. Quit" 202 | ########################################################## 203 | ########################## 4. Q ########################## 204 | ########################################################## 205 | else 206 | echo -e "${BLUE_BG}${BOLD}################# MAIN MENU ################${RESET}" 207 | 208 | if [ $ARE_DOMAINS_EMPTY == -1 ]; then 209 | echo "1. Add a site/domain" 210 | else 211 | echo "1. Manage sites/domains" 212 | fi 213 | 214 | echo "2. Re-configure rclone (remotes)" 215 | echo "3. Manage backups" 216 | echo "4. Quit" 217 | fi 218 | 219 | read -p "$(echo -e "${BOLD}${BLUE}Enter your choice: ${RESET}")" choice 220 | 221 | ########################################################## 222 | ########################## 1. A ########################## 223 | ########################################################## 224 | if [[ $ARE_DOMAINS_EMPTY == true && $ARE_DOMAINS_EMPTY != -1 ]]; then 225 | case "$choice" in 226 | 1) 227 | manage_domains 228 | ;; 229 | 2) 230 | # Quit 231 | clear_screen 232 | exit 0 233 | ;; 234 | *) 235 | # Show an error message if the used select invalid options 236 | clear_screen 237 | echo -e "${RED}Invalid choice. Please select a valid option.${RESET}" 238 | ;; 239 | esac 240 | ########################################################## 241 | ########################## 2. A ########################## 242 | ########################################################## 243 | elif [ $IS_RCLONE_CONFIGURED == false ]; then 244 | case "$choice" in 245 | 1) 246 | manage_domains 247 | ;; 248 | 2) 249 | configure_rclone 250 | ;; 251 | 3) 252 | # Quit 253 | clear_screen 254 | exit 0 255 | ;; 256 | *) 257 | # Show an error message if the used select invalid options 258 | clear_screen 259 | echo -e "${RED}Invalid choice. Please select a valid option.${RESET}" 260 | ;; 261 | esac 262 | ########################################################## 263 | ########################## 3. A ########################## 264 | ########################################################## 265 | elif [ $HAS_AUTOMATED_BACKUPS == false ]; then 266 | case "$choice" in 267 | 1) 268 | manage_domains 269 | ;; 270 | 2) 271 | configure_rclone 272 | ;; 273 | 3) 274 | generate_backup_script 275 | ;; 276 | 4) 277 | # Quit 278 | clear_screen # clear screen 279 | exit 0 280 | ;; 281 | *) 282 | # Show an error message if the used select invalid options 283 | clear_screen 284 | echo -e "${RED}Invalid choice. Please select a valid option.${RESET}" 285 | ;; 286 | esac 287 | 288 | ########################################################## 289 | ########################## 4. A ########################## 290 | ########################################################## 291 | else 292 | case "$choice" in 293 | 1) 294 | manage_domains 295 | ;; 296 | 2) 297 | configure_rclone 298 | ;; 299 | 3) 300 | manage_backups 301 | ;; 302 | 4) 303 | # Quit 304 | clear_screen # clear screen 305 | exit 0 306 | ;; 307 | *) 308 | # Show an error message if the used select invalid options 309 | clear_screen 310 | echo -e "${RED}Invalid choice. Please select a valid option.${RESET}" 311 | ;; 312 | esac 313 | fi 314 | done 315 | -------------------------------------------------------------------------------- /functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to clear the screen and position cursor at the top 4 | clear_screen_last_caller_name="" 5 | clear_screen() { 6 | local caller_name="${FUNCNAME[1]}" 7 | 8 | # Ignore if the previous caller function matches any of the arguements after $1 9 | # Good to avoid clearing the screen multiple times as part of the same menu tree. 10 | if [[ "$1" == "ignore" ]]; then 11 | for arg in "${@:2}"; do 12 | if [[ "$arg" == "$clear_screen_last_caller_name" ]]; then 13 | clear_screen_last_caller_name="$caller_name" 14 | return 15 | fi 16 | done 17 | fi 18 | 19 | # If "force" is specified, clear the screen regardless of the last caller, otherwise, don't clear twice for same caller 20 | if [[ "$1" = "force" || "$caller_name" != "$clear_screen_last_caller_name" ]]; then 21 | echo -e "\e[2J\e[H" 22 | clear_screen_last_caller_name="$caller_name" 23 | fi 24 | } 25 | 26 | # Save the cursor position 27 | save_cursor_position() { 28 | if [ "$1" == "alt" ]; then 29 | # echo -en "\e7" 30 | tput sc 31 | else 32 | echo -en "\e[s" 33 | fi 34 | } 35 | restore_cursor_position() { 36 | if [ "$1" == "alt" ]; then 37 | # Restore the cursor position and clear from there to the saved cursor position using the "alt" approach 38 | # echo -en "\e8\e[2J" 39 | tput rc 40 | tput ed 41 | # Re-save the cursor position 42 | # echo -en "\e7" 43 | tput sc 44 | else 45 | # Restore the cursor position and clear from there to the end of the screen (default approach) 46 | echo -en "\e[u\e[J" 47 | # Re-save the cursor position 48 | echo -en "\e[s" 49 | fi 50 | 51 | } 52 | # Function to save definitions to the file 53 | update_definitions() { 54 | 55 | # Remove leading and trailing spaces from elements and copy to new arrays 56 | local new_domains=() 57 | local new_paths=() 58 | for domain in "${DOMAINS[@]}"; do 59 | new_domains+=("\"${domain#"${domain%%[![:space:]]*}"}\"") # trip spaces and add inside double quote 60 | done 61 | for path in "${PATHS[@]}"; do 62 | new_paths+=("\"${path#"${path%%[![:space:]]*}"}\"") # trip spaces and add inside double quote 63 | done 64 | 65 | # Overwrite the definitions file 66 | cat <"$DEFINITIONS_FILE" 67 | # Definitions 68 | 69 | # Indexed arrays for domains and paths 70 | EOL 71 | 72 | if [ ${#new_domains[@]} -eq 0 ]; then 73 | echo "DOMAINS=()" >>"$DEFINITIONS_FILE" 74 | else 75 | echo "DOMAINS=(${new_domains[@]})" >>"$DEFINITIONS_FILE" 76 | fi 77 | 78 | if [ ${#new_paths[@]} -eq 0 ]; then 79 | echo "PATHS=()" >>"$DEFINITIONS_FILE" 80 | else 81 | echo "PATHS=(${new_paths[@]})" >>"$DEFINITIONS_FILE" 82 | fi 83 | 84 | # Backup configuration options 85 | cat <>"$DEFINITIONS_FILE" 86 | 87 | EOL 88 | 89 | # Update definitions state variables 90 | update_definitions_state 91 | } 92 | # Function to update the variables that represent the state of our definitions 93 | update_definitions_state() { 94 | 95 | # Check if DOMAINS array is empty 96 | ARE_DOMAINS_EMPTY=false 97 | if [ ${#DOMAINS[@]} -eq 0 ]; then 98 | ARE_DOMAINS_EMPTY=true 99 | fi 100 | 101 | # Check if rclone is configured by listing remotes 102 | IS_RCLONE_CONFIGURED=false 103 | if sudo rclone listremotes --long 2>&1 | grep -qEv 'NOTICE:'; then 104 | IS_RCLONE_CONFIGURED=true 105 | fi 106 | 107 | # Check if there are existing backups in the "cron_scripts" directory 108 | HAS_AUTOMATED_BACKUPS=false 109 | if [[ -d "$CRON_SCRIPTS_DIR" && -n "$(find "$CRON_SCRIPTS_DIR" -type f)" ]]; then 110 | HAS_AUTOMATED_BACKUPS=true 111 | fi 112 | 113 | # Update ARE_DOMAINS_EMPTY based on the value of HAS_AUTOMATED_BACKUPS to indicate automated backup exist, but domains don't 114 | if [[ $ARE_DOMAINS_EMPTY == true && $HAS_AUTOMATED_BACKUPS == true ]]; then 115 | ARE_DOMAINS_EMPTY=-1 116 | fi 117 | } 118 | 119 | # Function to add or update domain and user 120 | add_domain() { 121 | 122 | clear_screen "force" 123 | 124 | read -p "$(echo -e "${BOLD}${BLUE}Enter a domain${RESET} ${BLUE}( or q to go back ): ${RESET}")" domain 125 | 126 | if [ $domain == "q" ]; then 127 | manage_domains 128 | return 129 | fi 130 | 131 | # Clean and validate the domain 132 | local sanitized_domain=$(echo "$domain" | sed -e 's|^https://||' -e 's|^http://||' -e 's|^www\.||' -e 's|/.*$||') 133 | local domain_pattern="^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$" 134 | if [[ ! "$sanitized_domain" =~ $domain_pattern ]]; then 135 | clear_screen "force" 136 | echo -e "${RED}"${domain}" is not a valid domain.${RESET}" 137 | return 138 | fi 139 | 140 | # Check if the domain already exists in the DOMAINS array 141 | local duplicate=false 142 | for existing_domain in "${DOMAINS[@]}"; do 143 | if [ "$existing_domain" == "$sanitized_domain" ]; then 144 | duplicate=true 145 | break 146 | fi 147 | done 148 | # If the domain is already added, show an error 149 | if [ "$duplicate" == true ]; then 150 | clear_screen "force" 151 | echo -e "${RED}Domain $sanitized_domain is already in the list.${RESET}" 152 | return 153 | fi 154 | 155 | # Get the WordPress installation dir for that domain 156 | read -p "$(echo -e "${BOLD}${BLUE}Enter the WordPress installation's full path for $sanitized_domain${RESET} ${BLUE}( or q to go back ): ${RESET}")" path 157 | 158 | if [ $path == "q" ]; then 159 | add_domain 160 | return 161 | fi 162 | 163 | # Make sure the path always has a leading slash 164 | if [[ ! "$path" == /* ]]; then 165 | path="/$path" 166 | fi 167 | 168 | # Remove any trailing slash 169 | path="${path%/}" 170 | 171 | # Check for WordPress installation in different possible locations 172 | local wp_path="" 173 | 174 | # Check WordOps structure first (wp-config.php in parent, files in htdocs) 175 | if [ -f "$path/wp-config.php" ] && [ -d "$path/htdocs" ]; then 176 | wp_path="$path/htdocs" 177 | # Check if path points to htdocs directory 178 | elif [ -f "${path%/htdocs}/wp-config.php" ] && [ -d "$path" ]; then 179 | wp_path="$path" 180 | # Check standard structure (everything in same directory) 181 | elif [ -f "$path/wp-config.php" ]; then 182 | wp_path="$path" 183 | else 184 | # No WordPress installation found 185 | clear_screen "force" 186 | echo -e "${RED}We could not find a WordPress installation under $path ${RESET}" 187 | return 188 | fi 189 | 190 | # WordPress installation exists for this domain, let's append it to the array 191 | DOMAINS+=("$sanitized_domain") 192 | PATHS+=("$wp_path") # Store the path to WordPress files 193 | update_definitions # Save definitions after each addition 194 | clear_screen "force" 195 | echo -e "${GREEN}Domain $sanitized_domain added successfully.${RESET}" 196 | } 197 | 198 | # Function to delete domain and path 199 | delete_domain() { 200 | 201 | clear_screen "force" 202 | 203 | # Show a list of existing domains 204 | echo -e "${BOLD}The following is a list of the domains/sites available for deletion:${RESET}" 205 | for ((i = 0; i < ${#DOMAINS[@]}; i++)); do 206 | domain="${DOMAINS[$i]}" 207 | echo -e "- $domain" 208 | done 209 | 210 | # Ask user to type the domain they want to delete 211 | echo "" 212 | echo -e "${BOLD}${YELLOW}Note: ${RESET}${YELLOW}the automated backups created for the selected domain will NOT be effected.${RESET}" 213 | read -p "$(echo -e "${BOLD}${BLUE}Enter a domain to delete${RESET} ${BLUE}( or q to go back ): ${RESET}")" domain_to_delete 214 | 215 | if [ $domain_to_delete == "q" ]; then 216 | manage_domains 217 | return 218 | fi 219 | 220 | # Attempt to delete the domain from our list, and keep track 221 | local deleted=false 222 | local new_domains=() 223 | local new_paths=() 224 | 225 | for ((i = 0; i < ${#DOMAINS[@]}; i++)); do 226 | if [ "${DOMAINS[$i]}" != "$domain_to_delete" ]; then 227 | new_domains+=("${DOMAINS[$i]}") 228 | new_paths+=("${PATHS[$i]}") 229 | else 230 | deleted=true 231 | fi 232 | done 233 | 234 | # Run processes asscoaited with deletion, or raise an error 235 | if [ $deleted == true ]; then 236 | # Update the DOMAINS and PATHS arrays with the new values 237 | DOMAINS=("${new_domains[@]}") 238 | PATHS=("${new_paths[@]}") 239 | 240 | clear_screen "force" 241 | # Show success message 242 | echo -e "${GREEN}Domain $domain_to_delete has been deleted successfully.${RESET}" 243 | # Save definitions after each deletion 244 | update_definitions 245 | else 246 | 247 | clear_screen "force" 248 | # Show an error 249 | echo -e "${RED}Invalid choice. Domain $domain_to_delete is not in the list.${RESET}" 250 | fi 251 | 252 | } 253 | 254 | # Function to allow the management of domains and path ( view, add, delete ) 255 | manage_domains() { 256 | # If there are no existing domains/sites, fall back to the 'add_domain' function 257 | if [[ $ARE_DOMAINS_EMPTY == true || $ARE_DOMAINS_EMPTY == -1 ]]; then 258 | add_domain 259 | fi 260 | 261 | # Clear screen and show the site management menu 262 | clear_screen 263 | 264 | while true; do 265 | # Add a title 266 | echo -e "${BOLD}${UNDERLINE}Manage sites/domains${RESET}" 267 | # Show list of options 268 | local original_ps3="$PS3" # Save the original PS3 value 269 | PS3="$(echo -e "${BOLD}${BLUE}Type the desired option number to continue: ${RESET}")" 270 | options=("View existing domains/sites" "Add a new domain/site" "Delete an existing domain/site" "Return to the previous menu") 271 | select choice in "${options[@]}"; do 272 | case "$choice" in 273 | "View existing domains/sites") 274 | clear_screen "force" 275 | echo -e "${BOLD}The following is a list of the domain you've added:${RESET}" 276 | for ((i = 0; i < ${#DOMAINS[@]}; i++)); do 277 | domain="${DOMAINS[$i]}" 278 | path="${PATHS[$i]}" 279 | echo -e "- ${BOLD}Domain:${RESET} $domain, ${BOLD}Path:${RESET} $path" 280 | done 281 | echo "" 282 | ;; 283 | "Add a new domain/site") 284 | add_domain 285 | ;; 286 | "Delete an existing domain/site") 287 | delete_domain 288 | # Return to the main menu if there are no domains available 289 | if [[ $ARE_DOMAINS_EMPTY == true || $ARE_DOMAINS_EMPTY == -1 ]]; then 290 | return 291 | fi 292 | ;; 293 | "Return to the previous menu") 294 | clear_screen "force" 295 | return 296 | ;; 297 | *) 298 | clear_screen "force" 299 | echo -e "${RED}Invalid choice. Please select a valid option.${RESET}" 300 | ;; 301 | esac 302 | break 303 | done 304 | PS3="$original_ps3" # Restore the original PS3 value 305 | done 306 | 307 | } 308 | 309 | # A wrapper function that triggers `rclone config` to allow using to create new remotes, edit existing..etc 310 | configure_rclone() { 311 | 312 | clear_screen 313 | 314 | local configure_rclone=false 315 | # If there is an existing list of remotes, allow user to decide whether to configure rclone or not 316 | if sudo rclone listremotes --long 2>&1 | grep -qEv 'NOTICE:'; then 317 | # Add a title 318 | echo -e "${BOLD}${UNDERLINE}Re-configure rclone${RESET}" 319 | 320 | read -p "$(echo -e "${BOLD}${BLUE}Rclone has existing remotes, would you like to re-configure it (y/n): ${RESET}")" config 321 | if [ $config == "y" ] || [ $config == "yes" ]; then 322 | # Configure rclone 323 | configure_rclone=true 324 | fi 325 | else 326 | # Add a title 327 | echo -e "${BOLD}${UNDERLINE}Configure rclone${RESET}" 328 | 329 | # Configure rclone 330 | configure_rclone=true 331 | fi 332 | 333 | if [ $configure_rclone == true ]; then 334 | 335 | # Clear screen 336 | clear_screen "force" 337 | # Show a message 338 | echo -e "${YELLOW}Initializing rclone configuration ...${RESET}" 339 | echo "" 340 | # Start the configuration 341 | sudo rclone config 342 | fi 343 | 344 | # Clear screen 345 | clear_screen "force" 346 | 347 | # Update definitions state variables 348 | update_definitions_state 349 | } 350 | 351 | # Function to collect the backup settings from the user 352 | collect_backup_settings() { 353 | 354 | echo -e "${BOLD}${YELLOW}Step 1: ${RESET}${YELLOW}configure your backup settings.${RESET}" 355 | echo "" 356 | 357 | # Save the original PS3 value 358 | local original_ps3="$PS3" 359 | 360 | # Collect the target domain/site 361 | PS3="$(echo -e "${BOLD}${BLUE}Select the target domain for this backup: ${RESET}")" 362 | select BACKUP_DOMAIN in "${DOMAINS[@]}" "none"; do 363 | if [ "$BACKUP_DOMAIN" == "none" ]; then 364 | return 365 | elif [ -n "$BACKUP_DOMAIN" ]; then 366 | break 367 | else 368 | echo -e "${RED}Invalid option. Please select a valid number.${RESET}" 369 | fi 370 | done 371 | PS3="$original_ps3" # Restore the original PS3 value 372 | 373 | # Collect frequency preference 374 | PS3="$(echo -e "${BOLD}${BLUE}Choose backup frequency: ${RESET}")" 375 | select frequency in "daily" "weekly" "monthly" "none"; do 376 | case $frequency in 377 | daily | weekly | monthly) 378 | BACKUP_FREQUENCY="$frequency" 379 | break 380 | ;; 381 | none) 382 | return 383 | ;; 384 | *) 385 | echo -e "${RED}Invalid option. Please select a valid frequency.${RESET}" 386 | ;; 387 | esac 388 | done 389 | PS3="$original_ps3" # Restore the original PS3 value 390 | 391 | # Collect backup time preference 392 | PS3="$(echo -e "${BOLD}${BLUE}Choose backup time: ${RESET}")" 393 | options=("01:00" "02:00" "03:00" "04:00" "05:00" "06:00" "07:00" "08:00" "09:00" "10:00" "11:00" "12:00" "13:00" "14:00" "15:00" "16:00" "17:00" "18:00" "19:00" "20:00" "21:00" "22:00" "23:00" "00:00" "none") 394 | select time in "${options[@]}"; do 395 | case $time in 396 | "01:00" | "02:00" | "03:00" | "04:00" | "05:00" | "06:00" | "07:00" | "08:00" | "09:00" | "10:00" | "11:00" | "12:00" | "13:00" | "14:00" | "15:00" | "16:00" | "17:00" | "18:00" | "19:00" | "20:00" | "21:00" | "22:00" | "23:00" | "00:00") 397 | BACKUP_TIME="$time" 398 | break 399 | ;; 400 | "none") 401 | return 402 | ;; 403 | *) 404 | echo -e "${RED}Invalid option. Please select a valid time.${RESET}" 405 | ;; 406 | esac 407 | done 408 | PS3="$original_ps3" # Restore the original PS3 value 409 | 410 | # Collect retention period preference 411 | PS3="$(echo -e "${BOLD}${BLUE}Choose a retention period option: ${RESET}")" 412 | select retention in "3 days" "7 days" "30 days" "90 days" "180 days" "none"; do 413 | case $retention in 414 | "3 days" | "7 days" | "30 days" | "90 days" | "180 days") 415 | RETENTION_PERIOD="${retention%% *}" # Extract the numeric part 416 | break 417 | ;; 418 | none) 419 | return 420 | ;; 421 | *) 422 | echo -e "${RED}Invalid option. Please select a valid retention period.${RESET}" 423 | ;; 424 | esac 425 | done 426 | PS3="$original_ps3" # Restore the original PS3 value 427 | 428 | # Collect excluded folders 429 | read -p "$(echo -e "${BOLD}${BLUE}Enter folders to exclude (comma-separated eg; wp-admin, wp-includes; or leave empty for none): ${RESET}")" EXCLUDED_ITEMS 430 | 431 | # Collect backup type 432 | if [ $RESTIC_AVAILABLE == true ]; then 433 | PS3="$(echo -e "${BOLD}${BLUE}Choose backup type: ${RESET}")" 434 | select type in "full" "incremental"; do 435 | case $type in 436 | full | incremental) 437 | BACKUP_TYPE="$type" 438 | break 439 | ;; 440 | *) 441 | echo -e "${RED}Invalid option. Please select a valid type.${RESET}" 442 | ;; 443 | esac 444 | done 445 | else 446 | BACKUP_TYPE="full" 447 | fi 448 | 449 | # Collect restic password 450 | if [ $BACKUP_TYPE == "incremental" ]; then 451 | echo "" 452 | echo -e "${YELLOW}A password is required for incremental backups.${RESET}" 453 | echo -e "${BOLD}${YELLOW}Note 1:${RESET} ${YELLOW}The password is required by restic to take and restore backups.${RESET}" 454 | echo -e "${BOLD}${YELLOW}Note 2:${RESET} ${YELLOW}The Password will be saved in plain-text inside the backup script for automation.${RESET}" 455 | echo -e "${BOLD}${YELLOW}Note 3:${RESET} ${YELLOW}If the backup script is deleted, the password record will be lost.${RESET}" 456 | echo -e "${BOLD}${YELLOW}Note 4:${RESET} ${YELLOW}Make sure to remember your password, if it's lost/forgotten, the incremental backups cannot be restored anywhere.${RESET}" 457 | echo "" 458 | 459 | while true; do 460 | 461 | read -s -p "$(echo -e "${BOLD}${BLUE}Enter your password: ${RESET}")" BACKUP_PASS 462 | echo "" 463 | read -s -p "$(echo -e "${BOLD}${BLUE}Confirm your password: ${RESET}")" BACKUP_CONFIRM_PASS 464 | echo "" 465 | 466 | if [ "$BACKUP_PASS" == "$BACKUP_CONFIRM_PASS" ]; then 467 | break 468 | else 469 | echo -e "${RED}Passwords do not match. Please try again.${RESET}" 470 | fi 471 | done 472 | fi 473 | 474 | # Collect the destination folder path 475 | echo -e "${BLUE}- if using object based storage (eg; AWS S3, Google Cloud Storage..etc), the backup location should start with the 'bucket' name (eg; bucket/path/to/dir)${RESET}" 476 | echo -e "${BLUE}- If using SFTP/FTP based storage, use the full path to your backup directory (eg; /home/user/backup_folder)${RESET}" 477 | read -p "$(echo -e "${BOLD}${BLUE}Enter your backup location: ${RESET}")" REMOTE_BACKUP_LOCATION 478 | 479 | # If `$REMOTE_BACKUP_LOCATION` has a trailing slash, remove it 480 | if [[ -n "$REMOTE_BACKUP_LOCATION" && "$REMOTE_BACKUP_LOCATION" == */ ]]; then 481 | REMOTE_BACKUP_LOCATION="${REMOTE_BACKUP_LOCATION%/}" 482 | fi 483 | 484 | # Make sure all required settings are available, otherwise, re-collect them 485 | if [[ -z "$BACKUP_DOMAIN" || -z "$BACKUP_FREQUENCY" || -z "$BACKUP_TIME" || -z "$RETENTION_PERIOD" || -z "$BACKUP_TYPE" ]]; then 486 | 487 | clear_screen "force" 488 | echo -e "${RED}Something is missing, please select your preferences again.${RESET}" 489 | collect_backup_settings 490 | return 491 | fi 492 | 493 | # Ask the user to confirm their settings before proceeding 494 | echo "" 495 | echo -e "${YELLOW}Your current backup configurations:${RESET}" 496 | echo -e "${BOLD}Backup Site:${RESET} $BACKUP_DOMAIN" 497 | echo -e "${BOLD}Backup Frequency:${RESET} $BACKUP_FREQUENCY" 498 | echo -e "${BOLD}Backup Time:${RESET} $BACKUP_TIME" 499 | echo -e "${BOLD}Retention Period:${RESET} $RETENTION_PERIOD days" 500 | echo -e "${BOLD}Excluded Locations:${RESET} $EXCLUDED_ITEMS" 501 | echo -e "${BOLD}Remote Backup Location:${RESET} $REMOTE_BACKUP_LOCATION" 502 | echo -e "${BOLD}Remote Backup Type:${RESET} $BACKUP_TYPE" 503 | 504 | read -p "$(echo -e "${BOLD}${BLUE}Are you sure you want to proceed with the above configurations? (y/n): ${RESET}")" confirm 505 | if [[ $confirm != "y" && $confirm != "yes" ]]; then 506 | clear_screen "force" 507 | echo -e "${YELLOW}Please select your preferences again.${RESET}" 508 | collect_backup_settings 509 | return 510 | fi 511 | 512 | } 513 | 514 | generate_backup_script() { 515 | 516 | clear_screen 517 | 518 | # Add a title 519 | if [ $HAS_AUTOMATED_BACKUPS == false ]; then 520 | echo -e "${BOLD}${UNDERLINE}Create an automated backup${RESET}" 521 | else 522 | echo -e "${BOLD}${UNDERLINE}Manage backups > Create a new automated backup${RESET}" 523 | fi 524 | 525 | # Collect backup settings from user 526 | collect_backup_settings 527 | 528 | # Extract the collected backup settings from the `$backup_settings` array 529 | local backup_domain="${BACKUP_DOMAIN}" 530 | local backup_path="" 531 | local backup_frequency="${BACKUP_FREQUENCY}" 532 | local backup_time="${BACKUP_TIME}" 533 | local retention_period="${RETENTION_PERIOD}" 534 | local excluded_items="${EXCLUDED_ITEMS}" 535 | local remote_backup_location="${REMOTE_BACKUP_LOCATION}/" 536 | local remote_backup_type="${BACKUP_TYPE}" 537 | local restic_password="" 538 | local rclone_remote_name="" 539 | local rclone_remote_valid=false 540 | 541 | # Pull restic password if an incremental backup is defined 542 | if [ $remote_backup_type == "incremental" ]; then 543 | restic_password="${BACKUP_PASS}" 544 | fi 545 | 546 | # Validate backup_domain 547 | if [[ ! " ${DOMAINS[@]} " =~ " $backup_domain " ]]; then 548 | clear_screen "force" 549 | echo -e "${RED}A valid domain must be selected from the available options.${RESET}" 550 | return 551 | else 552 | # If the domain is valid, let populate the backup path 553 | for ((i = 0; i < ${#DOMAINS[@]}; i++)); do 554 | current_domain="${DOMAINS[$i]}" 555 | current_path="${PATHS[$i]}" 556 | if [ "$current_domain" == "$backup_domain" ]; then 557 | backup_path=$current_path 558 | fi 559 | done 560 | fi 561 | 562 | # Validate and sanitize backup_frequency 563 | if [ -z "$backup_frequency" ] || [[ ! "$backup_frequency" =~ ^(daily|weekly|monthly)$ ]]; then 564 | clear_screen "force" 565 | echo -e "${RED}A valid frequency must be selected from the available options.${RESET}" 566 | return 567 | fi 568 | 569 | # Validate backup_time 570 | if [ -z "$backup_time" ] || [[ ! "$backup_time" =~ ^[0-9]{2}:[0-9]{2}$ ]]; then 571 | clear_screen "force" 572 | echo -e "${RED}A valid backup time must selected from the available options.${RESET}" 573 | return 574 | else 575 | # Convert the user-provided time to the appropriate cron format 576 | IFS=: read -r cron_hour cron_minute <<<"$backup_time" 577 | fi 578 | 579 | # Validate retention_period 580 | if [ -z "$retention_period" ] || [[ ! "$retention_period" =~ ^(3|7|30|90|180)$ ]]; then 581 | clear_screen "force" 582 | echo -e "${RED}A valid retention period must selected from the available options.${RESET}" 583 | return 584 | fi 585 | 586 | # Create a cron expression based on the frequency and backup time 587 | case "$backup_frequency" in 588 | daily) 589 | cron_expression="$cron_minute $cron_hour * * *" 590 | ;; 591 | weekly) 592 | cron_expression="$cron_minute $cron_hour * * 0" # 0 represents Sunday, adjust as needed 593 | ;; 594 | monthly) 595 | cron_expression="$cron_minute $cron_hour 1 * *" # 1 represents first day of the month 596 | ;; 597 | *) 598 | # Handle invalid or unsupported frequency here 599 | echo -e "${RED}Invalid or unsupported frequency: $backup_frequency${RESET}" 600 | exit 1 601 | ;; 602 | esac 603 | 604 | # Prepare excludes by breaking the $excluded_items variable by comma and format based on backup type 605 | excludes="" 606 | if [ -n "$excluded_items" ]; then 607 | IFS=',' read -ra excluded_items_array <<<"$excluded_items" 608 | for item in "${excluded_items_array[@]}"; do 609 | # Remove leading and trailing spaces 610 | item=$(echo "$item" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') 611 | # Check if the item exists under the backup_domain path 612 | if [ -e "${backup_path}/${item}" ]; then 613 | # Wrap the item in double quotes and add it to excludes 614 | if [ $remote_backup_type == "incremental" ]; then 615 | excludes+=" --exclude=\"${backup_path}/${item}\"" 616 | else 617 | excludes+=" --exclude=\"${item}\"" 618 | fi 619 | 620 | else 621 | echo -e "${YELLOW}Warning: excluded location '${item}' does not exist under '${backup_path}'. Skipping...${RESET}" 622 | fi 623 | done 624 | fi 625 | 626 | clear_screen "force" 627 | echo -e "${BOLD}${YELLOW}Step 2: ${RESET}${YELLOW}select the rclone's remote you'd like to use for this backup.${RESET}" 628 | echo "" 629 | 630 | # Get comma seperated list of remotes to give to the user as examples 631 | local remotes_list=$(sudo rclone listremotes --long | awk -F ':' '{print $1}' | tr '\n' ', ') 632 | remotes_list="${remotes_list%,}" 633 | 634 | # Show available remotes 635 | echo -e "${BOLD}Available rclone remotes:${RESET}" 636 | sudo rclone listremotes --long 637 | 638 | # Use a while loop to make sure the user is prompted again if they made a mistake 639 | while true; do 640 | 641 | read -p "$(echo -e "${BOLD}${BLUE}Type the name of one of the available remotes as your backup destination (eg; ${remotes_list}): ${RESET}")" rclone_remote_name 642 | 643 | # Check if the entered remote name exists 644 | if rclone listremotes | grep -q "${rclone_remote_name}:"; then 645 | break # Remote name is valid, exit the loop 646 | else 647 | echo -e "${YELLOW}The remote you entered doesn't exist, please try again.${RESET}" 648 | fi 649 | done 650 | 651 | # Validate the rclone remote by writing a test file 652 | while [ $rclone_remote_valid == false ]; do 653 | sudo touch wpali.com.txt >/dev/null 2>&1 654 | # Run the rclone copy command and capture its exit code 655 | sudo rclone copy wpali.com.txt "${rclone_remote_name}":"${remote_backup_location}" 656 | exit_code=$? 657 | 658 | # Confirm the file has been created successfully on remote server 659 | if [ $exit_code -eq 0 ]; then 660 | # Get the rclone remote name that we'll use for the back up with this cron job 661 | echo "" 662 | echo -e "${GREEN}We have created a test file on your remote location.${RESET}" 663 | echo -e "Please go to ${BOLD}"$remote_backup_location"${RESET} on your remote server and check if this file ${BOLD}"wpali.com.txt"${RESET} exists." 664 | echo "" 665 | read -p "$(echo -e "${BOLD}${BLUE}Do you confirm that the test file has been created succesfully on your remote server? (y/n): ${RESET}")" rclone_remote_push_success 666 | 667 | if [[ -n "$rclone_remote_push_success" && ("$rclone_remote_push_success" == "y" || "$rclone_remote_push_success" == "yes") ]]; then 668 | # Copy was successful 669 | rclone_remote_valid=true 670 | # Delete leftovers 671 | sudo rm wpali.com.txt 672 | sudo rclone delete "${rclone_remote_name}":"${remote_backup_location}wpali.com.txt" 673 | fi 674 | else 675 | # Copy failed, display relevant errors 676 | echo -e "${RED}rclone copy failed with exit code $exit_code${RESET}" 677 | break 678 | fi 679 | done 680 | 681 | # Prepare the backup script and cron job based on frequency, time and the other options. 682 | if [ $rclone_remote_valid == true ]; then 683 | 684 | # Check if the cron scripts directory exists, and create it if it doesn't 685 | if [ ! -d "$CRON_SCRIPTS_DIR" ]; then 686 | mkdir -p "$CRON_SCRIPTS_DIR" 687 | fi 688 | 689 | # Generate a unique name for the backup script based on the frequency and time 690 | local creation_date=$(date +'%Y-%m-%d %H:%M:%S') 691 | local script_prefix=$(echo -n "$creation_date-$backup_domain-$cron_expression-$retention_period-$remote_backup_location-$rclone_remote_name-$remote_backup_type" | md5sum | awk '{print $1}') # used to prefix the backup script 692 | local script_name="${script_prefix}_${backup_domain//./-}_${backup_time//:/-}_${backup_frequency}_to_${rclone_remote_name}" 693 | local script_path="$CRON_SCRIPTS_DIR/$script_name" 694 | local script_hash=$(echo -n "$script_name" | md5sum | awk '{print $1}') # Use to prefix backups 695 | 696 | # Check if the file exists 697 | if [ -e "$script_path" ]; then 698 | echo -e "${RED}Duplicate detected, this backup could not be created.${RESET}" 699 | return 700 | fi 701 | 702 | # Initialize restic if this is an incremental backup 703 | if [ $remote_backup_type == "incremental" ]; then 704 | # Check if a restic repository already exists in the remote location 705 | local restic_remote_config_output=$(sudo RESTIC_PASSWORD="${restic_password}" restic -r "rclone:${rclone_remote_name}:${remote_backup_location}" cat config 2>&1) 706 | if echo "$restic_remote_config_output" | grep -q "Is there a repository at the following location"; then 707 | 708 | echo "" 709 | echo -e "${YELLOW}Initializing remote repository ...${RESET}" 710 | echo "" 711 | 712 | # If there are no errors, initialize the repo 713 | sudo RESTIC_PASSWORD="${restic_password}" restic -r "rclone:${rclone_remote_name}:${remote_backup_location}" init 714 | # Check the exit status of the previous command 715 | if [ $? -ne 0 ]; then 716 | # The restic init command failed, we'll bail out to avoid creating a non-valid backup script 717 | clear_screen "force" 718 | echo -e "${RED}Restic repository initilization failed.${RESET}" 719 | echo "" 720 | return 721 | fi 722 | else 723 | # If a repository does not exist, restic will return a non-zero exit code 724 | clear_screen "force" 725 | echo -e "${RED}This incremental backup could not be created. Duplicate found, or other error.${RESET}" 726 | echo "" 727 | return 728 | fi 729 | 730 | fi 731 | # Add the starting content for the backup script (eg; variables..etc) 732 | cat <"$script_path" 733 | #!/bin/bash 734 | 735 | # Define the call type, whether it's a pre-restore backup, or default backup 736 | if [ \$# -ge 1 ]; then 737 | call_type=\$1 738 | else 739 | call_type="backup" 740 | fi 741 | 742 | # Exit script on error, only if this is not a pre-restore backup 743 | if [ \${call_type} != "restore" ]; then 744 | set -e 745 | fi 746 | 747 | tmp_path=$TMP_DIR 748 | 749 | # Check if the tmp folder exists, and if not, create it 750 | if [ ! -d "\${tmp_path}" ]; then 751 | sudo mkdir -p "\${tmp_path}" 752 | fi 753 | 754 | # Find and delete any 'tar.gz.tmp' or 'sql' files that are older than 48 hours ( failed backups ) 755 | sudo find "\${tmp_path}" -type f -name "*.tar.gz.tmp" -mmin +2880 -exec rm {} \; 756 | sudo find "\${tmp_path}" -type f -name "*.sql" -mmin +2880 -exec rm {} \; 757 | 758 | # Make sure out logs files doesn't get too big 759 | if [ -f "$LOG_FILE" ]; then 760 | # Get the current size of the log file in bytes 761 | log_file_size=\$(stat -c %s "$LOG_FILE") 762 | log_file_max_size=1048576 # 1MB 763 | 764 | # Check if the log file size exceeds the maximum size ( 1048576 == 1MB ) 765 | if [ "\${log_file_size}" -gt "\${log_file_max_size}" ]; then 766 | # Truncate the log file to the maximum size 767 | tail -c "\${log_file_max_size}" "$LOG_FILE" > "$LOG_FILE.tmp" 768 | mv "$LOG_FILE.tmp" "$LOG_FILE" 769 | fi 770 | fi 771 | 772 | # Save all errors automatically in our logs file 773 | exec >> "$LOG_FILE" 2>&1 774 | 775 | # Prepare main variables 776 | type="${remote_backup_type}" 777 | hash="${script_hash}" 778 | creation_date="${creation_date}" 779 | cron_expression="${cron_expression}" 780 | domain="${backup_domain}" 781 | domain_path="${backup_path}" 782 | retention_period=${retention_period} 783 | rclone_remote="${rclone_remote_name}" 784 | remote_backup_location="${remote_backup_location}" 785 | timestamp=\$(date +'%Y-%m-%d %H:%M:%S') 786 | backup_date=\$(date +'%d-%m-%Y_%H-%M') 787 | 788 | echo "[\${timestamp}] BACK UP STARTED (\${type}): Performing $backup_time $backup_frequency backup for '\${domain}'" >> "$LOG_FILE" 789 | echo "[\${timestamp}] - Exporting database" >> "$LOG_FILE" 790 | 791 | EOF 792 | 793 | # Append to the script based on the backup type 794 | if [ $remote_backup_type == "incremental" ]; then 795 | # Add the necessary commands for incremental backups (append to file, note >>) 796 | cat <>"$script_path" 797 | # Get the wp installation folder owner and their home directory 798 | wp_owner=\$(sudo stat -c "%U" \${domain_path}) 799 | 800 | # Get the database name and construct the db backup file name and path 801 | # Note that we are using sudo to run wp cli commands as "wp_owner" to avoid permissions complications 802 | db_name=\$(sudo -u "\${wp_owner}" -s -- wp config get DB_NAME --path="\${domain_path}") 803 | db_filename=\${hash}_\${domain//./_}_\${db_name}_incremental.sql 804 | # We'll export the database and move it to our current directory as a 'tmp' file 805 | sudo -u "\${wp_owner}" -s -- wp db export "\${domain_path}/\${db_filename}" --path="\${domain_path}" 806 | 807 | restic_password=${restic_password} 808 | 809 | echo "[\${timestamp}] - Sending the backup to 'rclone' "\${rclone_remote}" remote using 'restic'" >>"$LOG_FILE" 810 | 811 | # Use restic to save a new backup to rclone remote 812 | if [ \${call_type} == "restore" ]; then 813 | # We will backup the whole folder when it's a pre-restore backup ( no excludes ) 814 | sudo RESTIC_PASSWORD="\${restic_password}" restic -q -r "rclone:\${rclone_remote}:\${remote_backup_location}" backup "\$domain_path/" 815 | else 816 | sudo RESTIC_PASSWORD="\${restic_password}" restic -q -r "rclone:\${rclone_remote}:\${remote_backup_location}" backup "\$domain_path/" ${excludes} 817 | fi 818 | 819 | echo "[\${timestamp}] - backup sent to the remote location successfully" >>"$LOG_FILE" 820 | echo "[\${timestamp}] - Delete the internally generated backup files to free space" >>"$LOG_FILE" 821 | 822 | # Delete the generated database file 823 | sudo rm "\${domain_path}/\${db_filename}" 824 | 825 | echo "[\${timestamp}] - Delete backups older than \${retention_period} days from 'rclone' "\${rclone_remote}" remote using 'restic'" >>"$LOG_FILE" 826 | 827 | # Delete old backups from remote ( retention logic ) 828 | sudo RESTIC_PASSWORD="\${restic_password}" restic -q -r "rclone:\${rclone_remote}:\${remote_backup_location}" forget --keep-within "\${retention_period}d" --prune 829 | 830 | EOF 831 | 832 | else 833 | # Add the necessary commands for full backups (append to file, note >>) 834 | cat <>"$script_path" 835 | 836 | # Get the wp installation folder owner 837 | wp_owner=\$(sudo stat -c "%U" \${domain_path}) 838 | 839 | # Use a dedicated temp directory for database backups 840 | wp_owner_directory="/tmp/wp_db_backup" 841 | 842 | echo "[\${timestamp}] - WP folder owner found: '\${wp_owner}'" >> "$LOG_FILE" 843 | echo "[\${timestamp}] - Using temp directory: '\${wp_owner_directory}'" >> "$LOG_FILE" 844 | 845 | # Create tmp directory with proper permissions if it doesn't exist 846 | if [ ! -d "\${wp_owner_directory}" ]; then 847 | sudo mkdir -p "\${wp_owner_directory}" 848 | sudo chown \${wp_owner}:\${wp_owner} "\${wp_owner_directory}" 849 | sudo chmod 755 "\${wp_owner_directory}" 850 | fi 851 | 852 | # Get wp-config.php location (one level up from domain_path for WordOps) 853 | wp_config_path="\${domain_path}" 854 | if [[ "\${domain_path}" == */htdocs ]]; then 855 | wp_config_path="\${domain_path%/htdocs}" 856 | fi 857 | 858 | # Get the database name and construct the db backup file name and path 859 | db_name=\$(sudo -u "\${wp_owner}" -s -- wp config get DB_NAME --path="\${domain_path}") 860 | db_filename=\${hash}_\${domain//./_}_\${db_name}_\${backup_date}.sql 861 | 862 | # Export the database with proper permissions 863 | sudo -u "\${wp_owner}" -s -- wp db export "\${wp_owner_directory}/\${db_filename}" --path="\${domain_path}" 864 | 865 | if [ \${call_type} == "restore" ]; then 866 | echo "[\${timestamp}] - Generating pre-restore backup archive" >> "$LOG_FILE" 867 | backup_filename=\${tmp_path}/\${hash}_\${domain//./-}_\${backup_date}-pre-restore.tar.gz 868 | else 869 | echo "[\${timestamp}] - Generating backup archive" >> "$LOG_FILE" 870 | backup_filename=\${tmp_path}/\${hash}_\${domain//./-}_\${backup_date}.tar.gz 871 | fi 872 | 873 | # --- Backup Logic --- 874 | 875 | # Prepare the backup file (compress the target site and the previously exported database) 876 | echo "[\${timestamp}] - Attempting direct tar backup" >> "$LOG_FILE" 877 | 878 | backup_success=false 879 | 880 | if sudo test "\${call_type}" = "restore"; then 881 | # Try direct tar for pre-restore backup first 882 | if sudo tar --warning=no-file-changed --transform 's,^\./,,' -czf "\${backup_filename}.tmp" -C "\${domain_path}/" . -C "\${wp_owner_directory}/" "\${db_filename}" 2>> "$LOG_FILE"; then 883 | backup_success=true 884 | echo "[\${timestamp}] - Direct tar backup successful" >> "$LOG_FILE" 885 | else 886 | echo "[\${timestamp}] - Direct tar backup failed, trying alternative method" >> "$LOG_FILE" 887 | fi 888 | else 889 | # Try direct tar with excludes first 890 | if sudo tar --warning=no-file-changed --transform 's,^\./,,' $excludes -czf "\${backup_filename}.tmp" -C "\${domain_path}/" . -C "\${wp_owner_directory}/" "\${db_filename}" 2>> "$LOG_FILE"; then 891 | backup_success=true 892 | echo "[\${timestamp}] - Direct tar backup successful" >> "$LOG_FILE" 893 | else 894 | echo "[\${timestamp}] - Direct tar backup failed, trying alternative method" >> "$LOG_FILE" 895 | fi 896 | fi 897 | 898 | # If direct tar failed, try cp method 899 | if [ "$backup_success" = false ]; then 900 | # Create temporary directory for cp method 901 | tmp_backup_dir="\${wp_owner_directory}/tmp_backup_\${backup_date}" 902 | 903 | # Check available space before copying 904 | required_space=$(sudo du -sb "${domain_path}" | cut -f1) 905 | available_space=$(sudo df -B1 "${wp_owner_directory}" | awk 'NR==2 {print $4}') 906 | 907 | if [ "$available_space" -gt "$((required_space * 2))" ]; then 908 | echo "[\${timestamp}] - Sufficient space available for cp method" >> "$LOG_FILE" 909 | 910 | # Create temp directory and copy files 911 | sudo mkdir -p "${tmp_backup_dir}" 912 | 913 | if sudo test "\${call_type}" = "restore"; then 914 | sudo cp -a "\${domain_path}/." "\${tmp_backup_dir}/" 915 | else 916 | sudo cp -a "\${domain_path}/." "\${tmp_backup_dir}/" 917 | # Apply excludes by removing excluded files/directories 918 | for exclude in $excludes; do 919 | exclude_path=$(echo "$exclude" | sed 's/--exclude=//') 920 | sudo rm -rf "\${tmp_backup_dir}/\${exclude_path}" 921 | done 922 | fi 923 | 924 | # Try tar on the copied files 925 | if sudo tar -czf "\${backup_filename}.tmp" -C "\${tmp_backup_dir}" . -C "\${wp_owner_directory}/" "\${db_filename}" 2>> "$LOG_FILE"; then 926 | backup_success=true 927 | echo "[\${timestamp}] - Backup successful using cp method" >> "$LOG_FILE" 928 | fi 929 | 930 | # Clean up temp directory 931 | sudo rm -rf "${tmp_backup_dir}" 932 | else 933 | echo "[\${timestamp}] - Insufficient space for cp method" >> "$LOG_FILE" 934 | fi 935 | fi 936 | 937 | if [ "$backup_success" = false ]; then 938 | echo "[\${timestamp}] ERROR: All backup methods failed" >> "$LOG_FILE" 939 | # Cleanup any temporary files 940 | sudo rm -f "\${backup_filename}.tmp" 941 | sudo rm -f "\${wp_owner_directory}/\${db_filename}" 942 | exit 1 943 | fi 944 | 945 | # Rename the temporary backup file to the actual name to indicate that the compression completed 946 | sudo mv "\${backup_filename}.tmp" "\${backup_filename}" 947 | 948 | # --- End Backup Logic --- 949 | 950 | echo "[\${timestamp}] - backup archive generated: "\${backup_filename}"" >> "$LOG_FILE" 951 | echo "[\${timestamp}] - Sending the backup file to remote location "\${rclone_remote}" using rclone" >> "$LOG_FILE" 952 | 953 | # Copy the generated backup to the remote location using rclone 954 | sudo rclone copy \$backup_filename \${rclone_remote}:\${remote_backup_location} 955 | 956 | echo "[\${timestamp}] - backup file sent to the remote location successfully" >> "$LOG_FILE" 957 | echo "[\${timestamp}] - Delete the internally generated backup files to free space" >> "$LOG_FILE" 958 | 959 | # Delete the generated backup archive and database 960 | sudo rm \${backup_filename} 961 | sudo rm \${wp_owner_directory}/\${db_filename} 962 | 963 | echo "[\${timestamp}] - Delete backups older than \${retention_period} days from remote location "\${rclone_remote}" using rclone" >> "$LOG_FILE" 964 | 965 | # Delete old backups from remote ( retention logic ) 966 | sudo rclone delete --min-age \${retention_period}d "\${rclone_remote}":"\${remote_backup_location}" 967 | 968 | EOF 969 | fi 970 | 971 | # Add the final content of the script file (append to file, note >>) 972 | cat <>"$script_path" 973 | echo "[\${timestamp}] BACK UP FINISHED (\${type}): $backup_frequency backup for \${domain} has completed successfully" >> "$LOG_FILE" 974 | 975 | # Make sure to close the log file when done 976 | exec 3>&- 977 | EOF 978 | 979 | # Give the script the right permissions ( only owner can read/write/execute ) 980 | sudo chmod 700 "$script_path" 981 | # Run the script to take the initial backup in the background 982 | sudo -b bash "$script_path" >/dev/null 2>&1 983 | # Create our cron file if it doesn't already exist, and give it correct permissions 984 | if [ ! -f "$CRON_FILE" ]; then 985 | sudo touch "$CRON_FILE" # Create the cron file 986 | sudo chmod 644 "$CRON_FILE" # Ensure the file has the correct permissions 987 | fi 988 | # Create the cron file with the necessary cron job 989 | echo "$cron_expression root /bin/bash $PWD/$script_path" >>"$CRON_FILE" 990 | # Show success message 991 | clear_screen "force" 992 | echo -e "${BOLD}${GREEN}Your automated backup for $backup_domain has been created successfully.${RESET}" 993 | echo -e "${GREEN}An initial backup is running the background.${RESET}" 994 | echo "" 995 | # Update definitions state variables 996 | update_definitions_state 997 | 998 | else 999 | # Copy failed, display relevant errors 1000 | echo -e "${RED}Something went wrong, please make sure the selected rclone remote is correctly configured.${RESET}" 1001 | echo -e "${RED}Also note that if you're using object based storage, your backup location should start with a bucket name.${RESET}" 1002 | read -p "$(echo -e "${BLUE}Would you like to open rclone configuration screen? (y/n): ${RESET}")" rclone_reconfig 1003 | if [ $rclone_reconfig == "y" ] || [ $rclone_reconfig == "yes" ]; then 1004 | configure_rclone 1005 | generate_backup_script 1006 | return 1007 | else 1008 | generate_backup_script 1009 | return 1010 | fi 1011 | fi 1012 | } 1013 | 1014 | # Function to manage local automated backups 1015 | manage_automated_backups() { 1016 | 1017 | # Exit on errors within this function 1018 | set -e 1019 | 1020 | # Clear screen and display a title 1021 | clear_screen 1022 | echo -e "${BOLD}${UNDERLINE}Manage backups > Manage automated backups${RESET}" 1023 | 1024 | # Check if a backup index is already provided. 1025 | # If index is provided, use it later to automatically display the selected backup instead of showing list of available backups 1026 | backup_already_selected=false 1027 | if [ $# -eq 1 ] && [[ $1 =~ ^[0-9]+$ ]]; then 1028 | # Assign the index to the variable 1029 | backup_already_selected=$1 1030 | fi 1031 | 1032 | # Initialize arrays to store backup details 1033 | declare -a backup_statuses 1034 | declare -a backup_scripts 1035 | declare -a backup_types 1036 | declare -a backup_cron_expressions 1037 | declare -a backup_hashes 1038 | declare -a backup_names 1039 | declare -a backup_schedules 1040 | declare -a backup_domains 1041 | declare -a backup_paths 1042 | declare -a remote_locations 1043 | declare -a rclone_remotes 1044 | declare -a retention_periods 1045 | 1046 | # Loop through existing backup scripts under $CRON_SCRIPTS_DIR 1047 | for script_file in "$CRON_SCRIPTS_DIR"/*; do 1048 | if [ -f "$script_file" ]; then 1049 | 1050 | local script_filename=$(echo "$(basename "$script_file")") 1051 | # Check if there is line with our script name in the cron file 1052 | local backup_schedule_line=$(grep -E ".*$script_filename" "$CRON_FILE" | grep -oP '(\S+ ){4}\S+') 1053 | 1054 | # Check if a valid line was found 1055 | local backup_status="Inactive" 1056 | if [ -n "$backup_schedule_line" ]; then 1057 | backup_status="Active" 1058 | fi 1059 | 1060 | # Extract other details from the backup script variables 1061 | local backup_type=$(grep -oP '(? Backup Details:${RESET}" 1179 | if [ "$selected_backup_status" == "Active" ]; then 1180 | echo -e "${BOLD}Backup Status:${RESET} ${GREEN}$selected_backup_status${RESET}" 1181 | else 1182 | echo -e "${BOLD}Backup Status:${RESET} ${YELLOW}$selected_backup_status${RESET}" 1183 | fi 1184 | echo -e "${BOLD}Backup Type:${RESET} ${BLUE}$selected_backup_type${RESET}" 1185 | echo -e "${BOLD}Backup ID:${RESET} $selected_backup_hash" 1186 | echo -e "${BOLD}Backup Name:${RESET} ${RESET} $selected_backup_name" 1187 | echo -e "${BOLD}Backup Schedule:${RESET} $selected_backup_schedule" 1188 | echo -e "${BOLD}Backup Domain:${RESET} $selected_backup_domain" 1189 | echo -e "${BOLD}Backup Path:${RESET} $selected_backup_path" 1190 | echo -e "${BOLD}Remote Location:${RESET} $selected_backup_remote_location" 1191 | echo -e "${BOLD}Rclone Remote:${RESET} $selected_backup_rclone_remote" 1192 | echo -e "${BOLD}Retention Period:${RESET} $selected_backup_retention_period days" 1193 | echo "" 1194 | 1195 | save_cursor_position 1196 | 1197 | # Save the original PS3 value 1198 | local original_ps3="$PS3" 1199 | while true; do 1200 | # Ask the user for further actions (e.g., delete, enable, disable) 1201 | options=("Enable" "Delete" "View/restore remote backups" "Return to the previous menu") 1202 | if [ "$selected_backup_status" == "Active" ]; then 1203 | options=("Disable" "Delete" "View/restore remote backups" "Return to the previous menu") 1204 | fi 1205 | 1206 | PS3="$(echo -e "${BOLD}${BLUE}Type the desired option number to continue: ${RESET}")" 1207 | select choice in "${options[@]}"; do 1208 | case "$choice" in 1209 | "Disable") 1210 | # Construct the cron pattern and remove the associated cron line from the specified cron file 1211 | cron_pattern="^${selected_backup_cron_expression//\*/\\*} .*$(basename "$selected_backup_script")" 1212 | sudo sed -i "/$cron_pattern/d" "$CRON_FILE" 1213 | 1214 | restore_cursor_position "alt" 1215 | clear_screen "force" 1216 | echo -e "${BOLD}${GREEN}'$selected_backup_name'${RESET} ${GREEN}has been disabled successfully.${RESET}" 1217 | manage_automated_backups "$selected_backup_index" # Reload this function with the current backup pre-selected 1218 | return 1219 | ;; 1220 | "Enable") 1221 | echo "$selected_backup_cron_expression root /bin/bash $PWD/$selected_backup_script" >>"$CRON_FILE" 1222 | 1223 | restore_cursor_position "alt" 1224 | clear_screen "force" 1225 | echo -e "${BOLD}${GREEN}'$selected_backup_name'${RESET} ${GREEN}has been enabled successfully.${RESET}" 1226 | manage_automated_backups "$selected_backup_index" # Reload this function with the current backup pre-selected 1227 | return 1228 | ;; 1229 | "Delete") 1230 | echo "" 1231 | echo -e "${RED_BG}---------------------------------------------------------------------------${RESET}" 1232 | echo -e "${RED_BG}--------------------------- PROCEED WITH CAUTION --------------------------${RESET}" 1233 | echo -e "${RED_BG}---------------------------------------------------------------------------${RESET}" 1234 | echo -e "${RED_BG}-------------- If you choose to delete this automated backup --------------${RESET}" 1235 | echo -e "${RED_BG}----------- you'll lose access to the backup restoration feature ----------${RESET}" 1236 | echo -e "${RED_BG}---------- and any management features associated with the backup ---------${RESET}" 1237 | echo -e "${RED_BG}---------------------------------------------------------------------------${RESET}" 1238 | echo "" 1239 | echo -e "${BOLD}${YELLOW}NOTE: ${RESET}You backup files on the remote server will not be effected.${RESET}" 1240 | echo "" 1241 | 1242 | # Confirm with the user before deleting the backup 1243 | read -p "$(echo -e "${BOLD}${RED}Choose an action (c: Confirm deletion, b: Bail out): ${RESET}")" confirm 1244 | if [ "$confirm" == "c" ]; then 1245 | # Remove the backup script file 1246 | sudo rm -f "$selected_backup_script" 1247 | 1248 | # Construct the cron pattern and remove the associated cron line from the specified cron file 1249 | cron_pattern="^${selected_backup_cron_expression//\*/\\*} .*$(basename "$selected_backup_script")" 1250 | sudo sed -i "/$cron_pattern/d" "$CRON_FILE" 1251 | 1252 | clear_screen "force" 1253 | echo -e "${BOLD}${GREEN}'$selected_backup_name'${RESET} ${GREEN}has been deleted successfully.${RESET}" 1254 | update_definitions_state 1255 | manage_automated_backups 1256 | return 1257 | 1258 | else 1259 | restore_cursor_position 1260 | echo -e "${BOLD}${YELLOW}'$selected_backup_name'${RESET} ${YELLOW}deletion has been aborted.${RESET}" 1261 | echo "" 1262 | fi 1263 | ;; 1264 | "View/restore remote backups") 1265 | 1266 | if [ $selected_backup_type == "incremental" ]; then 1267 | 1268 | echo "" 1269 | echo -e "${YELLOW}Pulling remote incremental backups ...${RESET}" 1270 | echo "" 1271 | 1272 | # Extract repo password from backup file ( may not have double quotes ) 1273 | local restic_password=$(grep -oP 'restic_password=("[^"]*"|\S+)' "$selected_backup_script" | cut -d '=' -f 2) 1274 | 1275 | # List existing snapshots 1276 | sudo RESTIC_PASSWORD="${restic_password}" restic -r "rclone:${selected_backup_rclone_remote}:${selected_backup_remote_location}" snapshots 1277 | 1278 | # Ask the user to select a backup for restoration 1279 | read -p "$(echo -e "${BOLD}${BLUE}Enter the ID of the backup you'd like to restore ( or q to go back ): ${RESET}")" selected_remote_backup 1280 | 1281 | # Go back if the user typed q 1282 | if [ $selected_remote_backup == "q" ]; then 1283 | restore_cursor_position 1284 | break # break out of the select statement to restart the while loop 1285 | fi 1286 | 1287 | # Confirm with the user before restoring the backup 1288 | echo "" 1289 | echo -e "You selected: ${BOLD}$selected_remote_backup${RESET}" 1290 | echo -e "Choose a restore approach:" 1291 | echo -e "${BOLD}${YELLOW}1. ${RESET}Restore only" 1292 | echo -e "${BOLD}${YELLOW}2. ${RESET}Clear and restore" 1293 | 1294 | read -p "$(echo -e "${BOLD}${BLUE}Enter the number of your choice (1/2)${RESET} ${BLUE}( or q to go back ): ${RESET}")" restore_approach_choice 1295 | 1296 | # Go back if the user typed q 1297 | if [ $restore_approach_choice == "q" ]; then 1298 | restore_cursor_position 1299 | break # break out of the select statement to restart the while loop 1300 | fi 1301 | 1302 | # Handle user restore choice 1303 | if [[ $restore_approach_choice == "1" || $restore_approach_choice == "2" ]]; then 1304 | restore_cursor_position 1305 | 1306 | # Show a pre-restore backup notice 1307 | echo "" 1308 | echo -e "${YELLOW}Taking a pre-restore backup ...${RESET}" 1309 | # Take a backup using backup script with the arg "restore" to indicate this is a pre-restore backup 1310 | sudo bash $selected_backup_script "restore" 1311 | 1312 | # Clear the destination folder if "clear and restore is selected" 1313 | if [[ $restore_approach_choice == "2" ]]; then 1314 | sudo rm -rf "${selected_backup_path%/}"/* 1315 | fi 1316 | 1317 | # Show a restoration notice 1318 | echo "" 1319 | echo -e "${YELLOW}Restoring${RESET} ${BOLD}${YELLOW}$selected_remote_backup${RESET} ${YELLOW}to:${RESET} ${BOLD}${YELLOW}$selected_backup_path${RESET}" 1320 | 1321 | # Restore to the same backed up path 1322 | # Use --target to manipulate the destination 1323 | # Use --include to only include specific folder or file from snapshot 1324 | # use ":path/to/folder" after the snapshot ID to restore the content of a specific folder directly 1325 | sudo RESTIC_PASSWORD="${restic_password}" restic -r "rclone:${selected_backup_rclone_remote}:${selected_backup_remote_location}" restore $selected_remote_backup --target "/" 1326 | else 1327 | restore_cursor_position 1328 | echo -e "${YELLOW}restore has been aborted.${RESET}" 1329 | echo "" 1330 | fi 1331 | else 1332 | 1333 | echo "" 1334 | echo -e "${YELLOW}Pulling remote backups count & total size...${RESET}" 1335 | 1336 | # Show backups size and count 1337 | echo "" 1338 | sudo rclone size "${selected_backup_rclone_remote}":"${selected_backup_remote_location}" --include "${selected_backup_hash}_*" 1339 | 1340 | echo "" 1341 | echo -e "${YELLOW}Pulling remote backups list...${RESET}" 1342 | 1343 | # Capture the list of backup files 1344 | local backup_list_output=$(sudo rclone ls "${selected_backup_rclone_remote}":"${selected_backup_remote_location}" --include "${selected_backup_hash}_*") 1345 | 1346 | # Check if the backup list is empty 1347 | if [ -z "$backup_list_output" ]; then 1348 | restore_cursor_position 1349 | echo -e "${YELLOW}No remote backups found.${RESET}" 1350 | echo "" 1351 | break # break out of the select statement to restart the while loop 1352 | fi 1353 | 1354 | # Capture the list of remote backup files 1355 | local remote_backup_files=() 1356 | local remote_backup_lines=() 1357 | while IFS= read -r line; do 1358 | # Remove leading spaces from the line 1359 | line="${line#"${line%%[![:space:]]*}"}" 1360 | 1361 | # Extract the size and filename from the line 1362 | local remote_backup_size="${line%% *}" # Extract size (everything before the first space) 1363 | local remote_backup_name="${line#* }" # Extract filename (everything after the first space) 1364 | 1365 | # Remove the MD5 prefix from remote_backup_name 1366 | local noprefix_remote_backup_name="${remote_backup_name#*_}" 1367 | 1368 | # Extract the date and time from the filename 1369 | if [[ "$noprefix_remote_backup_name" =~ ([0-9]{2}-[0-9]{2}-[0-9]{4})_([0-9]{2}-[0-9]{2}).*\.tar\.gz ]]; then 1370 | backup_file_date="${BASH_REMATCH[1]}" 1371 | backup_file_time="${BASH_REMATCH[2]//-/:}" 1372 | 1373 | # Format the time to display in 12-hour format with AM/PM 1374 | backup_file_time=$(date -d "$backup_file_time" +"%I:%M%p") 1375 | 1376 | # Format the size in a human-readable format (MB, GB, etc.) 1377 | backup_size_readable=$(numfmt --to=iec --suffix=B --format="%.2f" "$remote_backup_size") 1378 | 1379 | # Increment the index for numbering the options 1380 | if [ $index == 1 ]; then 1381 | backup_file_date="$backup_file_date-15455" 1382 | fi 1383 | # Add the formatted line to the remote_backup_files array 1384 | remote_backup_files+=("$remote_backup_name") 1385 | remote_backup_lines+=("$backup_file_date $backup_file_time $backup_size_readable $noprefix_remote_backup_name") 1386 | fi 1387 | done <<<"$backup_list_output" 1388 | 1389 | # Display the backup list as a table with aligned headers 1390 | echo "" 1391 | echo -e "${BOLD}${YELLOW}# Date Time Size Name${RESET}" 1392 | # Calculate the maximum length of the index numbers to align them properly 1393 | local max_index_length="${#remote_backup_lines[@]}" 1394 | while ((max_index_length > 0)); do 1395 | max_index_length=$((max_index_length / 10)) 1396 | local index_length=$((index_length + 1)) 1397 | done 1398 | 1399 | for ((i = 0; i < ${#remote_backup_lines[@]}; i++)); do 1400 | # Calculate the padding for the index numbers 1401 | local padding_length=$((index_length - ${#i})) 1402 | local padding="" 1403 | for ((j = 0; j < padding_length; j++)); do 1404 | padding+=" " 1405 | done 1406 | local item_index=$((i + 1)) 1407 | echo -e "${BOLD}${YELLOW}${padding}${item_index}. ${RESET}${remote_backup_lines[i]}" 1408 | done | column -t 1409 | 1410 | # Ask the user to select a backup for restoration 1411 | read -p "$(echo -e "${BOLD}${BLUE}Enter the number of the backup to restore (1-${#remote_backup_lines[@]}) ${BLUE}( or q to go back ): ${RESET}")" restore_choice 1412 | 1413 | # Validate the user's choice 1414 | if [[ ! "$restore_choice" =~ ^[0-9]+$ ]] || [ "$restore_choice" -lt 1 ] || [ "$restore_choice" -gt "${#remote_backup_lines[@]}" ]; then 1415 | restore_cursor_position 1416 | echo -e "${RED}Invalid choice. Please enter a valid number.${RESET}" 1417 | break # break out of the select statement to restart the while loop 1418 | fi 1419 | 1420 | # Go back if the user typed q 1421 | if [ $restore_choice == "q" ]; then 1422 | restore_cursor_position 1423 | break # break out of the select statement to restart the while loop 1424 | fi 1425 | 1426 | # Get the selected backup based on the user's choice 1427 | local selected_remote_backup="${remote_backup_files[restore_choice - 1]}" 1428 | 1429 | # Confirm with the user before restoring the backup 1430 | echo "" 1431 | echo -e "You selected: ${BOLD}$selected_remote_backup${RESET}" 1432 | echo -e "Choose a restore approach:" 1433 | echo -e "${BOLD}${YELLOW}1. ${RESET}Restore only" 1434 | echo -e "${BOLD}${YELLOW}2. ${RESET}Clear and restore" 1435 | 1436 | read -p "$(echo -e "${BOLD}${BLUE}Enter the number of your choice (1/2)${RESET} ${BLUE}( or q to go back ): ${RESET}")" restore_approach_choice 1437 | # Go back if the user typed q 1438 | if [ $restore_approach_choice == "q" ]; then 1439 | restore_cursor_position 1440 | break # break out of the select statement to restart the while loop 1441 | fi 1442 | 1443 | # Handle user restore choice 1444 | if [[ $restore_approach_choice == "1" || $restore_approach_choice == "2" ]]; then 1445 | restore_cursor_position 1446 | 1447 | # Show a pre-restore backup notice 1448 | echo "" 1449 | echo -e "${YELLOW}Taking a pre-restore backup ...${RESET}" 1450 | # Take a backup using backup script with the arg "restore" to indicate this is a pre-restore backup 1451 | sudo bash $selected_backup_script "restore" 1452 | 1453 | # Show a restoration notice 1454 | echo "" 1455 | echo -e "${YELLOW}Restoring ${RESET}${BOLD}${YELLOW}$selected_remote_backup${RESET} ${YELLOW}to${RESET} ${BOLD}${YELLOW}$selected_backup_path${RESET}" 1456 | # Pull the backup from remote 1457 | sudo rclone copyto --progress "${selected_backup_rclone_remote}":"${selected_backup_remote_location}${selected_remote_backup}" "${TMP_DIR}/${selected_remote_backup}.tmp" 1458 | 1459 | # Handle "Clear and restore" option 1460 | if [ $restore_approach_choice == "2" ]; then 1461 | sudo rm -rf "${selected_backup_path%/}"/* 1462 | fi 1463 | 1464 | # Unzip the backup file inside the destination folder 1465 | sudo tar -xzf "${TMP_DIR}/${selected_remote_backup}.tmp" -C "$selected_backup_path" 1466 | sudo rm "${TMP_DIR}/${selected_remote_backup}.tmp" 1467 | 1468 | else 1469 | restore_cursor_position 1470 | echo -e "${BOLD}${YELLOW}'$selected_remote_backup'${RESET} ${YELLOW}restoration has been aborted.${RESET}" 1471 | echo "" 1472 | fi 1473 | fi 1474 | 1475 | # Show a db import notice 1476 | echo "" 1477 | echo -e "${YELLOW}Importing the database ...${RESET}" 1478 | # Now import the database using wp cli and delete it afterwards 1479 | local wp_owner=$(sudo stat -c "%U" ${selected_backup_path}) # get WordPress folder owner 1480 | local sql_file=$(find "$selected_backup_path" -type f -name "*${selected_backup_hash}_*.sql" -print -quit) # find the sql file path 1481 | sudo -u "${wp_owner}" -i -- wp db import "${sql_file}" --path="${selected_backup_path}" # import db 1482 | sudo rm "${sql_file}" # Delete the SQL file after it's been imported 1483 | 1484 | clear_screen "force" 1485 | # Show a success message 1486 | echo -e "${BOLD}${GREEN}Restore successfully completed.${RESET}" 1487 | echo "" 1488 | manage_automated_backups 1489 | return 1490 | ;; 1491 | "Return to the previous menu") 1492 | restore_cursor_position "alt" 1493 | clear_screen "force" 1494 | manage_automated_backups 1495 | return 1496 | ;; 1497 | *) 1498 | restore_cursor_position 1499 | echo -e "${RED}Invalid action. Please choose a valid action.${RESET}" 1500 | ;; 1501 | esac 1502 | break 1503 | done 1504 | done 1505 | 1506 | # Restore the original PS3 value 1507 | PS3="$original_ps3" 1508 | } 1509 | 1510 | # Function to manage automated backups 1511 | manage_backups() { 1512 | while true; do 1513 | 1514 | # Go back to the main menu in case all backups has been deleted 1515 | if [ $HAS_AUTOMATED_BACKUPS == false ]; then 1516 | clear_screen 1517 | return 1518 | fi 1519 | 1520 | # Shpw the backup management menu 1521 | clear_screen "ignore" "generate_backup_script" "manage_automated_backups" 1522 | echo -e "${BOLD}${UNDERLINE}Manage backups${RESET}" 1523 | echo "1. Manage automated backups" 1524 | echo "2. Create a new automated backup" 1525 | echo "3. Return to the previous menu" 1526 | read -p "$(echo -e "${BOLD}${BLUE}Enter your choice: ${RESET}")" choice 1527 | 1528 | case "$choice" in 1529 | 1) 1530 | manage_automated_backups 1531 | ;; 1532 | 2) 1533 | generate_backup_script 1534 | ;; 1535 | 3) 1536 | clear_screen "force" 1537 | return 1538 | ;; 1539 | *) 1540 | clear_screen "force" 1541 | echo -e "${RED}Invalid choice. Please select a valid option.${RESET}" 1542 | ;; 1543 | esac 1544 | done 1545 | } 1546 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Rclone Automated Backups for WordPress 2 | 3 | Automate WordPress backups to various cloud storage providers using rclone automation. Also supports `restic` for incremental backups using rclone. 4 | 5 | ![Screenshot](/screenshot.png) 6 | 7 | ## Requirements 8 | - Sudo and SSH access to your server. 9 | - [wp-cli](https://wp-cli.org/) installed. 10 | - [rclone](https://rclone.org/) installed. 11 | 12 | ## Optional 13 | - [restic](https://restic.readthedocs.io/en/stable/020_installation.html) to add incremental backup support ( no setup or configuration needed ). 14 | 15 | ## Getting Started 16 | 17 | Follow these steps to set up automated WordPress backups: 18 | 19 | ### How to use: 20 | - Connect to your server via SSH: `ssh root@server.ip.address` 21 | - Download this repo zip file: 22 | 23 | ```shell 24 | apt-get -y install wget git 25 | git clone https://github.com/bomsn/rclone-automated-backups-for-wordpress.git rclone-wordpress 26 | ``` 27 | - Run the initilization script 28 | 29 | ```shell 30 | cd rclone-wordpress 31 | sudo bash config.sh 32 | ``` 33 | 34 | You'll have options to add domains and configure backups. 35 | 36 | The script will guide you through the process, just make sure to add sites/domains and their correct path, add backup configurations such as backup time, frequency, retention period, and configure rclone remote(s), then create your backups. 37 | 38 | **Note:** the domains and associated paths will be saved to `definitions` file, you can change it later if needed. However, note that changing the file doesn't change any running backups. 39 | 40 | That's it, once you've completed all the configuration steps, a cron job will be created to take backups automatically using rclone. Feel free to use the menu again to make as many automated backups as you want. 41 | 42 | ### Subsequent Use: 43 | 44 | If you want to add more websites, create additional backups, disable or delete existing backups, or even restore the remote backups created by the script, just run the config script again `sudo bash config.sh` and use the available options. 45 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bomsn/rclone-automated-backups-for-wordpress/ce7ba91e5bcfd43c8770d01d7565c66d6d6fa142/screenshot.png --------------------------------------------------------------------------------