From e3311aef9470230c876aaeb04020093f1401c058 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Mon, 19 Aug 2024 22:59:31 +0300 Subject: [PATCH 01/31] Adding `print_usage` function to `backup_and_restore.sh` script --- helper-scripts/backup_and_restore.sh | 33 ++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index dc30d5ea10..7de1fc6498 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -1,5 +1,28 @@ #!/usr/bin/env bash +# --------------------------------------------------- +# Here's only the functions that are optimized, +# checked and refactored. +# +# This comment will be deleted once I finished +# working with this file. +# --------------------------------------------------- + +# ----------------- Start Functions ----------------- + +function print_usage() { + echo "Usage: ${0} [option] [argument]" + echo + echo "Options:" + echo -e " backup\t[crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days]" + echo -e " restore" + echo + echo "Environment Variables:" + echo -e " THREADS\tnum\tNumber of threads" +} + +# ----------------- End Functions ----------------- + DEBIAN_DOCKER_IMAGE="mailcow/backup:latest" if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then @@ -7,12 +30,18 @@ if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then fi if [[ ! ${1} =~ (backup|restore) ]]; then - echo "First parameter needs to be 'backup' or 'restore'" + print_usage exit 1 fi if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days) ]]; then - echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql', 'all' or '--delete-days'" + if [[ -z "${2}" ]]; then + echo -e "\e[31mRequired argument for backup option\e[0m\n" + else + echo -e "\e[31mUnknown argument: ${2}\e[0m\n" + fi + + print_usage exit 1 fi From 2460a98f20b0fa5a13a67489234f4045e2ab0017 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Mon, 19 Aug 2024 23:33:35 +0300 Subject: [PATCH 02/31] Add colored messages in `backup_and_restore.sh` script --- helper-scripts/backup_and_restore.sh | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 7de1fc6498..47f277b07a 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -52,17 +52,17 @@ if [[ -z ${BACKUP_LOCATION} ]]; then fi if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then - echo "Backup directory needs to be given as absolute path (starting with /)." + echo -e "\e[31mBackup directory needs to be given as absolute path (starting with /).\e[0m" exit 1 fi if [[ -f ${BACKUP_LOCATION} ]]; then - echo "${BACKUP_LOCATION} is a file!" + echo -e "\e[31m${BACKUP_LOCATION} is a file!\e[" exit 1 fi if [[ ! -d ${BACKUP_LOCATION} ]]; then - echo "${BACKUP_LOCATION} is not a directory" + echo -e "\e[33m${BACKUP_LOCATION} is not a directory\e[0m" read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then exit 1 @@ -72,7 +72,8 @@ if [[ ! -d ${BACKUP_LOCATION} ]]; then fi else if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a ${BACKUP_LOCATION}) | grep -oE '[0-9][0-9][5-7]') ]]; then - echo "${BACKUP_LOCATION} is not write-able for others, that's required for a backup." + echo -e "\e[31m${BACKUP_LOCATION} is not write-able for others, that's required for a backup.\e[0m" + echo "Execute \`chmod 755 ${BACKUP_LOCATION}\` and try again." exit 1 fi fi @@ -85,33 +86,33 @@ THREADS=$(echo ${THREADS:-1}) ARCH=$(uname -m) if ! [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then - echo "Thread input is not a number!" + echo -e "\e[31mThread input is not a number!\e[0m" exit 1 elif [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then - echo "Using ${THREADS} Thread(s) for this run." - echo "Notice: You can set the Thread count with the THREADS Variable before you run this script." + echo -e "\e[32mUsing ${THREADS} Thread(s) for this run.\e[0m" + echo -e "\e[33mNotice: You can set the Thread count with the THREADS Variable before you run this script.\e[0m" fi if [ ! -f ${COMPOSE_FILE} ]; then - echo "Compose file not found" + echo -e "\e[31mCompose file not found\e[0m" exit 1 fi if [ ! -f ${ENV_FILE} ]; then - echo "Environment file not found" + echo -e "\e[31mEnvironment file not found\e[0m" exit 1 fi -echo "Using ${BACKUP_LOCATION} as backup/restore location." +echo -e "\e[33mUsing ${BACKUP_LOCATION} as backup/restore location.\e[0m" echo source ${SCRIPT_DIR}/../mailcow.conf if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then - echo "Could not determine compose project name" + echo -e "\e[31mCould not determine compose project name\e[0m" exit 1 else - echo "Found project name ${COMPOSE_PROJECT_NAME}" + echo -e "\e[32mFound project name ${COMPOSE_PROJECT_NAME}\e[0m" CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") fi @@ -169,11 +170,11 @@ function backup() { mysql|all) SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) if [[ -z "${SQLIMAGE}" ]]; then - echo "Could not determine SQL image version, skipping backup..." + echo -e "\e[31mCould not determine SQL image version, skipping backup...\e[0m" shift continue else - echo "Using SQL image ${SQLIMAGE}, starting..." + echo -e "\e[32mUsing SQL image ${SQLIMAGE}, starting...\e[0m" docker run --name mailcow-backup --rm \ --network $(docker network ls -qf name=^${CMPS_PRJ}_mailcow-network$) \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:ro,z \ @@ -191,7 +192,7 @@ function backup() { if [[ "${1}" =~ ^[0-9]+$ ]]; then find ${BACKUP_LOCATION}/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \; else - echo "Parameter of --delete-days is not a number." + echo -e "\e[31mParameter of --delete-days is not a number.\e[0m" fi ;; esac @@ -219,7 +220,7 @@ function restore() { fi echo - echo "Stopping watchdog-mailcow..." + echo -e "\e[33mStopping watchdog-mailcow...\e[0m" docker stop $(docker ps -qf name=watchdog-mailcow) echo RESTORE_LOCATION="${1}" @@ -297,11 +298,11 @@ function restore() { mysql|mariadb) SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) if [[ -z "${SQLIMAGE}" ]]; then - echo "Could not determine SQL image version, skipping restore..." + echo -e "\e[31mCould not determine SQL image version, skipping restore...\e[0m" shift continue elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then - echo "Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore." + echo -e "\e[31mCould not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore.\e[0m" echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process." shift continue @@ -369,7 +370,7 @@ elif [[ ${1} == "restore" ]]; then i=1 declare -A FOLDER_SELECTION if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then - echo "Selected backup location has no subfolders" + echo -e "\e[31mSelected backup location has no subfolders\e[0m" exit 1 fi for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do @@ -387,7 +388,7 @@ elif [[ ${1} == "restore" ]]; then declare -A FILE_SELECTION RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then - echo "No datasets found" + echo -e "\e[31mNo datasets found\e[0m" exit 1 fi From 5eceecf8cc50f8ea60a9bd6e98a954fd96bb6d96 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Mon, 19 Aug 2024 23:38:37 +0300 Subject: [PATCH 03/31] Move the `thread count notice` into the usage menu --- helper-scripts/backup_and_restore.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 47f277b07a..abacce2299 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -18,7 +18,7 @@ function print_usage() { echo -e " restore" echo echo "Environment Variables:" - echo -e " THREADS\tnum\tNumber of threads" + echo -e " THREADS\t\tYou can set the thread count with the THREADS environment variable before you run this script." } # ----------------- End Functions ----------------- @@ -90,7 +90,6 @@ if ! [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then exit 1 elif [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then echo -e "\e[32mUsing ${THREADS} Thread(s) for this run.\e[0m" - echo -e "\e[33mNotice: You can set the Thread count with the THREADS Variable before you run this script.\e[0m" fi if [ ! -f ${COMPOSE_FILE} ]; then From 82def75ec557fdb2d7f4066100466c1f2f8fffda Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Mon, 19 Aug 2024 23:47:10 +0300 Subject: [PATCH 04/31] Fix: Double quoted variable `BACKUP_LOCATION`. Since it might have spaces, it must be double quoted. --- helper-scripts/backup_and_restore.sh | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index abacce2299..da1e6c2b45 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -25,7 +25,7 @@ function print_usage() { DEBIAN_DOCKER_IMAGE="mailcow/backup:latest" -if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then +if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" fi @@ -45,40 +45,40 @@ if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all| exit 1 fi -if [[ -z ${BACKUP_LOCATION} ]]; then - while [[ -z ${BACKUP_LOCATION} ]]; do +if [[ -z "${BACKUP_LOCATION}" ]]; then + while [[ -z "${BACKUP_LOCATION}" ]]; do read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION done fi -if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then +if [[ ! "${BACKUP_LOCATION}" =~ ^/ ]]; then echo -e "\e[31mBackup directory needs to be given as absolute path (starting with /).\e[0m" exit 1 fi -if [[ -f ${BACKUP_LOCATION} ]]; then +if [[ -f "${BACKUP_LOCATION}" ]]; then echo -e "\e[31m${BACKUP_LOCATION} is a file!\e[" exit 1 fi -if [[ ! -d ${BACKUP_LOCATION} ]]; then +if [[ ! -d "${BACKUP_LOCATION}" ]]; then echo -e "\e[33m${BACKUP_LOCATION} is not a directory\e[0m" read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then exit 1 else - mkdir -p ${BACKUP_LOCATION} - chmod 755 ${BACKUP_LOCATION} + mkdir -p "${BACKUP_LOCATION}" + chmod 755 "${BACKUP_LOCATION}" fi else - if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a ${BACKUP_LOCATION}) | grep -oE '[0-9][0-9][5-7]') ]]; then + if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a "${BACKUP_LOCATION}") | grep -oE '[0-9][0-9][5-7]') ]]; then echo -e "\e[31m${BACKUP_LOCATION} is not write-able for others, that's required for a backup.\e[0m" echo "Execute \`chmod 755 ${BACKUP_LOCATION}\` and try again." exit 1 fi fi -BACKUP_LOCATION=$(echo ${BACKUP_LOCATION} | sed 's#/$##') +BACKUP_LOCATION=$(echo "${BACKUP_LOCATION}" | sed 's#/$##') SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml ENV_FILE=${SCRIPT_DIR}/../.env @@ -137,32 +137,32 @@ function backup() { case "$1" in vmail|all) docker run --name mailcow-backup --rm \ - -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \ ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail ;;& crypt|all) docker run --name mailcow-backup --rm \ - -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \ ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt ;;& redis|all) docker exec $(docker ps -qf name=redis-mailcow) redis-cli save docker run --name mailcow-backup --rm \ - -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \ ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis ;;& rspamd|all) docker run --name mailcow-backup --rm \ - -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \ ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd ;;& postfix|all) docker run --name mailcow-backup --rm \ - -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \ ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix ;;& @@ -179,7 +179,7 @@ function backup() { -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:ro,z \ -t --entrypoint= \ --sysctl net.ipv6.conf.all.disable_ipv6=1 \ - -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ ${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \ mariabackup --prepare --target-dir=/backup_mariadb ; \ chown -R 999:999 /backup_mariadb ; \ @@ -189,7 +189,7 @@ function backup() { --delete-days) shift if [[ "${1}" =~ ^[0-9]+$ ]]; then - find ${BACKUP_LOCATION}/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \; + find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \; else echo -e "\e[31mParameter of --delete-days is not a number.\e[0m" fi @@ -368,11 +368,11 @@ if [[ ${1} == "backup" ]]; then elif [[ ${1} == "restore" ]]; then i=1 declare -A FOLDER_SELECTION - if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then + if [[ $(find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then echo -e "\e[31mSelected backup location has no subfolders\e[0m" exit 1 fi - for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do + for folder in $(ls -d "${BACKUP_LOCATION}"/mailcow-*/); do echo "[ ${i} ] - ${folder}" FOLDER_SELECTION[${i}]="${folder}" ((i++)) From a621aea4510ae39703133f09a1d7f9b0aea4db2e Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 02:45:54 +0300 Subject: [PATCH 05/31] Bug Fix: overwriting filesystem possible Previous code was not checking if the path is a `symbolic` to a filesystem, if user is root and the given path is a `symbolic` of filesystem then it will overwrite it! --- helper-scripts/backup_and_restore.sh | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index da1e6c2b45..4414efa7db 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -56,26 +56,35 @@ if [[ ! "${BACKUP_LOCATION}" =~ ^/ ]]; then exit 1 fi -if [[ -f "${BACKUP_LOCATION}" ]]; then - echo -e "\e[31m${BACKUP_LOCATION} is a file!\e[" - exit 1 -fi - -if [[ ! -d "${BACKUP_LOCATION}" ]]; then - echo -e "\e[33m${BACKUP_LOCATION} is not a directory\e[0m" +# if "${BACKUP_LOCATION}" not exists, create it. +if [[ ! -e "${BACKUP_LOCATION}" ]]; then + echo -e "\e[33m${BACKUP_LOCATION} is not exist\e[0m" read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then - exit 1 + echo -e "\e[33mExiting without creating the backup location.\e[0m" + exit 0 else mkdir -p "${BACKUP_LOCATION}" chmod 755 "${BACKUP_LOCATION}" + + if [[ ! "${?}" -eq 0 ]]; then + echo -e "\e[31mFailed, check the error above!\e[0m" + exit 1 + fi fi -else +# if "${BACKUP_LOCATION}" is an exists directory, +# then just check the permissions +elif [[ -d "${BACKUP_LOCATION}" ]]; then + echo -e "\e[32mFound directory ${BACKUP_LOCATION}\e[0m" if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a "${BACKUP_LOCATION}") | grep -oE '[0-9][0-9][5-7]') ]]; then echo -e "\e[31m${BACKUP_LOCATION} is not write-able for others, that's required for a backup.\e[0m" echo "Execute \`chmod 755 ${BACKUP_LOCATION}\` and try again." exit 1 fi +# else, the "${BACKUP_LOCATION}" is something else! an alien? yes! +else + echo -e "\e[31m${BACKUP_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[" + exit 1 fi BACKUP_LOCATION=$(echo "${BACKUP_LOCATION}" | sed 's#/$##') From 0d95962f1c09b07bd07eacdad167b7723901ce72 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 02:53:35 +0300 Subject: [PATCH 06/31] Simplify the check expression of `THREADS` --- helper-scripts/backup_and_restore.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 4414efa7db..fe6673a5a9 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -94,11 +94,11 @@ ENV_FILE=${SCRIPT_DIR}/../.env THREADS=$(echo ${THREADS:-1}) ARCH=$(uname -m) -if ! [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then +if [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]]; then + echo -e "\e[32mUsing ${THREADS} thread(s) for this run.\e[0m" +else echo -e "\e[31mThread input is not a number!\e[0m" exit 1 -elif [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then - echo -e "\e[32mUsing ${THREADS} Thread(s) for this run.\e[0m" fi if [ ! -f ${COMPOSE_FILE} ]; then From 67ba81a41c7c1d6181ed373f0862f5eca4593af2 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 03:16:43 +0300 Subject: [PATCH 07/31] Implement `check_required_tools` function Implement `check_required_tools` function in `backup_and_restore.sh` script, as well call it in the beginning of the script instead of at the end --- helper-scripts/backup_and_restore.sh | 38 +++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index fe6673a5a9..ff9094cdf3 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -21,8 +21,27 @@ function print_usage() { echo -e " THREADS\t\tYou can set the thread count with the THREADS environment variable before you run this script." } +function check_required_tools() { + # Add the required tools to the array + local required_tools=("docker") + + for bin in "${required_tools[@]}"; do + if [[ -z $(which ${bin}) ]]; then + echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + exit 1 + fi + done + + if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then + echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" + exit 1 + fi +} + # ----------------- End Functions ----------------- +check_required_tools + DEBIAN_DOCKER_IMAGE="mailcow/backup:latest" if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then @@ -124,11 +143,6 @@ else CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") fi -if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then - >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" - exit 1 -fi - function backup() { DATE=$(date +"%Y-%m-%d-%H-%M-%S") @@ -136,12 +150,7 @@ function backup() { chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" - for bin in docker; do - if [[ -z $(which ${bin}) ]]; then - >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" - exit 1 - fi - done + while (( "$#" )); do case "$1" in vmail|all) @@ -209,13 +218,6 @@ function backup() { } function restore() { - for bin in docker; do - if [[ -z $(which ${bin}) ]]; then - >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" - exit 1 - fi - done - if [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then COMPOSE_COMMAND="docker compose" From f64efb3b1dd5f46de22775f8884d8bd8229d5679 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 18:00:11 +0300 Subject: [PATCH 08/31] Clarifying the duplicate assigning of `MAILCOW_BACKUP_LOCATION` --- helper-scripts/backup_and_restore.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index ff9094cdf3..3b7f085734 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -44,6 +44,8 @@ check_required_tools DEBIAN_DOCKER_IMAGE="mailcow/backup:latest" +# This duplicated assignment is to address +# issue in `https://github.com/mailcow/mailcow-dockerized/issues/957` if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" fi From cfe13b0d9e5f9691e4ba3d74bcb44bb06549fbf5 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 18:48:14 +0300 Subject: [PATCH 09/31] Fix Bug: `backup_and_restore.sh` script, increment `i` only if needed --- helper-scripts/backup_and_restore.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 3b7f085734..6053ee0285 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -379,16 +379,16 @@ function restore() { if [[ ${1} == "backup" ]]; then backup ${@,,} elif [[ ${1} == "restore" ]]; then - i=1 + i=0 declare -A FOLDER_SELECTION if [[ $(find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then echo -e "\e[31mSelected backup location has no subfolders\e[0m" exit 1 fi for folder in $(ls -d "${BACKUP_LOCATION}"/mailcow-*/); do + ((i++)) echo "[ ${i} ] - ${folder}" FOLDER_SELECTION[${i}]="${folder}" - ((i++)) done echo input_sel=0 From 4d00ca6dbfa92934013de4fe448055f354292dcf Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 19:15:03 +0300 Subject: [PATCH 10/31] Fix: Crash on entering non-numeric value `backup_and_restore.sh` script, with `restore` flag, crashes when entering a non-numeric value as backup choice --- helper-scripts/backup_and_restore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 6053ee0285..4c37ba8006 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -392,7 +392,7 @@ elif [[ ${1} == "restore" ]]; then done echo input_sel=0 - while [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do + while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do read -p "Select a restore point: " input_sel done i=1 From a2ab5271bc4e5a1c9f5fb31996024c006210d037 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 20 Aug 2024 20:55:24 +0300 Subject: [PATCH 11/31] Fix: backup_location should be exists, for the restore --- helper-scripts/backup_and_restore.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 4c37ba8006..a6ae703023 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -79,6 +79,11 @@ fi # if "${BACKUP_LOCATION}" not exists, create it. if [[ ! -e "${BACKUP_LOCATION}" ]]; then + if [[ "${1}" == "restore" ]]; then + echo -e "\e[31m${BACKUP_LOCATION} is not exists\e[0m" + exit 1 + fi + echo -e "\e[33m${BACKUP_LOCATION} is not exist\e[0m" read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then From ea24505f35d4b1022865a77e06ff57c1aa0011b2 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 00:20:49 +0300 Subject: [PATCH 12/31] Fix: Crash on entering non-numeric value Fix the crash on entering non-numeric value, as well as make the code more readable --- helper-scripts/backup_and_restore.sh | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index a6ae703023..74f56b307b 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -400,58 +400,64 @@ elif [[ ${1} == "restore" ]]; then while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do read -p "Select a restore point: " input_sel done - i=1 + echo - declare -A FILE_SELECTION + RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then echo -e "\e[31mNo datasets found\e[0m" exit 1 fi + i=0 + declare -A FILE_SELECTION + echo "[ 0 ] - all" # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do if [[ ${file} =~ vmail ]]; then + ((i++)) echo "[ ${i} ] - Mail directory (/var/vmail)" FILE_SELECTION[${i}]="vmail" - ((i++)) elif [[ ${file} =~ crypt ]]; then + ((i++)) echo "[ ${i} ] - Crypt data" FILE_SELECTION[${i}]="crypt" - ((i++)) elif [[ ${file} =~ redis ]]; then + ((i++)) echo "[ ${i} ] - Redis DB" FILE_SELECTION[${i}]="redis" - ((i++)) elif [[ ${file} =~ rspamd ]]; then if [[ $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + ((i++)) echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)" FILE_SELECTION[${i}]="rspamd" - ((i++)) elif [[ $ARCH != $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m" else + ((i++)) echo "[ ${i} ] - Rspamd data" FILE_SELECTION[${i}]="rspamd" - ((i++)) fi elif [[ ${file} =~ postfix ]]; then + ((i++)) echo "[ ${i} ] - Postfix data" FILE_SELECTION[${i}]="postfix" - ((i++)) elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then + ((i++)) echo "[ ${i} ] - SQL DB" FILE_SELECTION[${i}]="mysql" - ((i++)) fi done + echo + input_sel=-1 - while [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do + while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do read -p "Select a dataset to restore: " input_sel done + echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} fi From c45b860f561bf8d8da8eb9bbb2250a956760773f Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 03:33:47 +0300 Subject: [PATCH 13/31] Implement `declare_restore_point` function Refactor `backup_and_restore.sh` script, implement `declare_restore_point` function. --- helper-scripts/backup_and_restore.sh | 80 ++++++++++++++++++---------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 74f56b307b..1bccecf747 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -38,6 +38,51 @@ function check_required_tools() { fi } +# declare_restore_point declares the `RESTORE_POINT` +# globally which is the path to the backup folder. +# +# If the function succeeds, the variable `RESTORE_POINT` +# will be declared globally. +# If the function fails, it will exit with a status of 1. +function declare_restore_point() { + local BACKUP_LOCATION="${1}" + + # Check subfolders inside BACKUP_LOCATION + if [[ $(find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then + echo -e "\e[31mSelected backup location has no subfolders\e[0m" + exit 1 + fi + + local i=0 + local -A FOLDER_SELECTION + + # Loop through the folders inside BACKUP_LOCATION, + # and print them to stdout, for the user to choose. + for folder in $(ls -d "${BACKUP_LOCATION}"/mailcow-*/); do + ((i++)) + echo "[ ${i} ] - ${folder}" + FOLDER_SELECTION[${i}]="${folder}" + done + + echo + + # Prompt the user to choose what to restore. + local input_sel=0 + while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do + read -p "Select a restore point: " input_sel + done + + echo + + # Declare the RESTORE_POINT variable globally, + # to be used outside the function. + RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" + if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then + echo -e "\e[31mNo datasets found\e[0m" + exit 1 + fi +} + # ----------------- End Functions ----------------- check_required_tools @@ -384,38 +429,17 @@ function restore() { if [[ ${1} == "backup" ]]; then backup ${@,,} elif [[ ${1} == "restore" ]]; then - i=0 - declare -A FOLDER_SELECTION - if [[ $(find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then - echo -e "\e[31mSelected backup location has no subfolders\e[0m" - exit 1 - fi - for folder in $(ls -d "${BACKUP_LOCATION}"/mailcow-*/); do - ((i++)) - echo "[ ${i} ] - ${folder}" - FOLDER_SELECTION[${i}]="${folder}" - done - echo - input_sel=0 - while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do - read -p "Select a restore point: " input_sel - done - - echo - - RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" - if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then - echo -e "\e[31mNo datasets found\e[0m" - exit 1 - fi + # Caling `declare_restore_point` will + # declare `RESTORE_POINT` globally. + declare_restore_point "${BACKUP_LOCATION}" i=0 declare -A FILE_SELECTION echo "[ 0 ] - all" # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz - FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') - for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do + FILE_SELECTION[0]=$(find "${RESTORE_POINT}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') + for file in $(ls -f "${RESTORE_POINT}"); do if [[ ${file} =~ vmail ]]; then ((i++)) echo "[ ${i} ] - Mail directory (/var/vmail)" @@ -429,11 +453,11 @@ elif [[ ${1} == "restore" ]]; then echo "[ ${i} ] - Redis DB" FILE_SELECTION[${i}]="redis" elif [[ ${file} =~ rspamd ]]; then - if [[ $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + if [[ $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then ((i++)) echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)" FILE_SELECTION[${i}]="rspamd" - elif [[ $ARCH != $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then + elif [[ $ARCH != $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m" else ((i++)) From de743cd2bf11461fa8b9474ba72b1f1a81302ba1 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 14:34:42 +0300 Subject: [PATCH 14/31] Typo in `calling declare_restore_point` --- helper-scripts/backup_and_restore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 1bccecf747..ee3259f3c9 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -429,7 +429,7 @@ function restore() { if [[ ${1} == "backup" ]]; then backup ${@,,} elif [[ ${1} == "restore" ]]; then - # Caling `declare_restore_point` will + # Calling `declare_restore_point` will # declare `RESTORE_POINT` globally. declare_restore_point "${BACKUP_LOCATION}" From 728a9951aff5f942ee691311adaaaa0152f4c2da Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 16:39:50 +0300 Subject: [PATCH 15/31] Fix: `grep: warning: stray \ before :` --- helper-scripts/backup_and_restore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index ee3259f3c9..3dfffa9224 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -358,7 +358,7 @@ function restore() { docker start $(docker ps -aqf name=postfix-mailcow) ;; mysql|mariadb) - SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) + SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) if [[ -z "${SQLIMAGE}" ]]; then echo -e "\e[31mCould not determine SQL image version, skipping restore...\e[0m" shift From d398ecaf0890a0bb28355e3fc4d925d9ee30c2b2 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 16:42:41 +0300 Subject: [PATCH 16/31] Implement `declare_file_selection` function Refactor `backup_and_restore.sh` script, implement `declare_file_selection` function --- helper-scripts/backup_and_restore.sh | 120 ++++++++++++++++----------- 1 file changed, 70 insertions(+), 50 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 3dfffa9224..4c404a93d7 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -83,6 +83,71 @@ function declare_restore_point() { fi } +# declare_file_selection declares the `FILE_SELECTION` +# globally which is the path to the backup folder inside +# `RESTORE_POINT` folder (which components to restore from). +# +# If the function succeeds, the variable `FILE_SELECTION` +# will be declared globally. +# If the function fails, it will exit with a status of 1. +function declare_file_selection() { + local RESTORE_POINT="$1" + + local i=0 + local -A FILE_SELECTIONS + + echo "[ ${i} ] - all" + + # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz + FILE_SELECTIONS[0]=$(find "${RESTORE_POINT}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') + for file in $(ls -f "${RESTORE_POINT}"); do + if [[ ${file} =~ vmail ]]; then + ((i++)) + echo "[ ${i} ] - Mail directory (/var/vmail)" + FILE_SELECTIONS[${i}]="vmail" + elif [[ ${file} =~ crypt ]]; then + ((i++)) + echo "[ ${i} ] - Crypt data" + FILE_SELECTIONS[${i}]="crypt" + elif [[ ${file} =~ redis ]]; then + ((i++)) + echo "[ ${i} ] - Redis DB" + FILE_SELECTIONS[${i}]="redis" + elif [[ ${file} =~ rspamd ]]; then + if [[ $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + ((i++)) + echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)" + FILE_SELECTIONS[${i}]="rspamd" + elif [[ $ARCH != $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then + echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m" + else + ((i++)) + echo "[ ${i} ] - Rspamd data" + FILE_SELECTIONS[${i}]="rspamd" + fi + elif [[ ${file} =~ postfix ]]; then + ((i++)) + echo "[ ${i} ] - Postfix data" + FILE_SELECTIONS[${i}]="postfix" + elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then + ((i++)) + echo "[ ${i} ] - SQL DB" + FILE_SELECTIONS[${i}]="mysql" + fi + done + + echo + + # Prompt the user to choose what to restore. + input_sel=-1 + while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do + read -p "Select a dataset to restore: " input_sel + done + + # Declare the global variable + FILE_SELECTION="${FILE_SELECTIONS[${input_sel}]}" +} + # ----------------- End Functions ----------------- check_required_tools @@ -433,55 +498,10 @@ elif [[ ${1} == "restore" ]]; then # declare `RESTORE_POINT` globally. declare_restore_point "${BACKUP_LOCATION}" - i=0 - declare -A FILE_SELECTION - - echo "[ 0 ] - all" - # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz - FILE_SELECTION[0]=$(find "${RESTORE_POINT}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') - for file in $(ls -f "${RESTORE_POINT}"); do - if [[ ${file} =~ vmail ]]; then - ((i++)) - echo "[ ${i} ] - Mail directory (/var/vmail)" - FILE_SELECTION[${i}]="vmail" - elif [[ ${file} =~ crypt ]]; then - ((i++)) - echo "[ ${i} ] - Crypt data" - FILE_SELECTION[${i}]="crypt" - elif [[ ${file} =~ redis ]]; then - ((i++)) - echo "[ ${i} ] - Redis DB" - FILE_SELECTION[${i}]="redis" - elif [[ ${file} =~ rspamd ]]; then - if [[ $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then - ((i++)) - echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)" - FILE_SELECTION[${i}]="rspamd" - elif [[ $ARCH != $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then - echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m" - else - ((i++)) - echo "[ ${i} ] - Rspamd data" - FILE_SELECTION[${i}]="rspamd" - fi - elif [[ ${file} =~ postfix ]]; then - ((i++)) - echo "[ ${i} ] - Postfix data" - FILE_SELECTION[${i}]="postfix" - elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then - ((i++)) - echo "[ ${i} ] - SQL DB" - FILE_SELECTION[${i}]="mysql" - fi - done - - echo - - input_sel=-1 - while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do - read -p "Select a dataset to restore: " input_sel - done + # Calling `declare_file_selection` will + # declare `FILE_SELECTION` globally. + declare_file_selection "${RESTORE_POINT}" - echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." - restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} + echo "Restoring ${FILE_SELECTION} from ${RESTORE_POINT}..." + restore "${RESTORE_POINT}" "${FILE_SELECTION}" fi From bd229b9dbc5fb7b8d3f8dd4974966d49e734e978 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 18:29:28 +0300 Subject: [PATCH 17/31] Implement `restore_docker_component` to restore components in one place Implement `restore_docker_component` function to restore `vmail`, `redis`, `crypt`, `rspamd` and `postfix`, all from one function --- helper-scripts/backup_and_restore.sh | 84 +++++++++++++--------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 4c404a93d7..76093ac764 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -1,13 +1,5 @@ #!/usr/bin/env bash -# --------------------------------------------------- -# Here's only the functions that are optimized, -# checked and refactored. -# -# This comment will be deleted once I finished -# working with this file. -# --------------------------------------------------- - # ----------------- Start Functions ----------------- function print_usage() { @@ -148,6 +140,38 @@ function declare_file_selection() { FILE_SELECTION="${FILE_SELECTIONS[${input_sel}]}" } +function restore_docker_component() { + local RESTORE_LOCATION="${1}" + local DOCKER_COMPOSE_PROJECT_NAME="${2}" + # Example: dovecot-mailcow, postfix-mailcow, redis-mailcow + local DOCKER_IMAGE_NAME="${3}" + # Example: vmail, redis, crypt, rspamd and postfix + local COMPONENT_NAME="${4}" + + local CONTAINER_ID + local DOCKER_VOLUME_NAME + + CONTAINER_ID="$(docker ps -qf name="${DOCKER_IMAGE_NAME}")" + DOCKER_VOLUME_NAME="$(docker volume ls -qf name=^${DOCKER_COMPOSE_PROJECT_NAME}_${COMPONENT_NAME}-vol-1$)" + + if [[ -z "${DOCKER_VOLUME_NAME}" ]]; then + echo + echo -e "\e[31mFatal Error: Docker volume for [${DOCKER_COMPOSE_PROJECT_NAME}_${DOCKER_IMAGE_NAME}] not found!\e[0m" + exit 1 + fi + + # Restoring Process + + docker stop "${CONTAINER_ID}" + + docker run -i --name mailcow-backup --rm \ + -v "${RESTORE_LOCATION}:/backup:z" \ + -v "${DOCKER_VOLUME_NAME}:/${COMPONENT_NAME}:z" \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_"${COMPONENT_NAME}".tar.gz + + docker start "${CONTAINER_ID}" +} + # ----------------- End Functions ----------------- check_required_tools @@ -355,12 +379,8 @@ function restore() { while (( "$#" )); do case "$1" in vmail) - docker stop $(docker ps -qf name=dovecot-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz - docker start $(docker ps -aqf name=dovecot-mailcow) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "dovecot-mailcow" "vmail" + echo echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" echo @@ -374,20 +394,10 @@ function restore() { fi ;; redis) - docker stop $(docker ps -qf name=redis-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz - docker start $(docker ps -aqf name=redis-mailcow) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "redis-mailcow" "redis" ;; crypt) - docker stop $(docker ps -qf name=dovecot-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz - docker start $(docker ps -aqf name=dovecot-mailcow) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "dovecot-mailcow" "crypt" ;; rspamd) if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then @@ -395,32 +405,18 @@ function restore() { sleep 2 echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" sleep 2 - docker stop $(docker ps -qf name=rspamd-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz - docker start $(docker ps -aqf name=rspamd-mailcow) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" + elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." sleep 2 echo -e "Skipping rspamd due to compatibility issues!\e[0m" else - docker stop $(docker ps -qf name=rspamd-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz - docker start $(docker ps -aqf name=rspamd-mailcow) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" fi ;; postfix) - docker stop $(docker ps -qf name=postfix-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz - docker start $(docker ps -aqf name=postfix-mailcow) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "postfix-mailcow" "postfix" ;; mysql|mariadb) SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) From d0b38b7910c3962219aa8789aca871fe6dbfb0cb Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 18:30:01 +0300 Subject: [PATCH 18/31] Fix last `grep: warning: stray \ before :` --- helper-scripts/backup_and_restore.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 76093ac764..7ebccdba24 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -291,7 +291,7 @@ function backup() { chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" - + while (( "$#" )); do case "$1" in vmail|all) @@ -326,7 +326,7 @@ function backup() { ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix ;;& mysql|all) - SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) + SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) if [[ -z "${SQLIMAGE}" ]]; then echo -e "\e[31mCould not determine SQL image version, skipping backup...\e[0m" shift From 93fc601b0b607c54b6724d5dc749bc1decd31217 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Wed, 21 Aug 2024 18:34:08 +0300 Subject: [PATCH 19/31] Write a clarification comment for function `restore_docker_component` --- helper-scripts/backup_and_restore.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 7ebccdba24..4cc5c78bee 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -140,6 +140,17 @@ function declare_file_selection() { FILE_SELECTION="${FILE_SELECTIONS[${input_sel}]}" } +# restore_docker_component restores components used in +# docker compose project name to the specified path. +# +# Parameters: +# 1. RESTORE_LOCATION +# 2. DOCKER_COMPOSE_PROJECT_NAME +# 3. DOCKER_IMAGE_NAME +# 4. COMPONENT_NAME +# +# If the function succeeds, will return with no value. +# If the function fails, it will exit with a status of 1. function restore_docker_component() { local RESTORE_LOCATION="${1}" local DOCKER_COMPOSE_PROJECT_NAME="${2}" From 78a62663ce90c63a67ccf602c64670c0e9cd0481 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Thu, 22 Aug 2024 22:06:40 +0300 Subject: [PATCH 20/31] Introduce new `help` screen, with new flags depends script - Add New help screen/menu - Script now works with flags, such as `-r|--restore`, `-b|--backup` and so on. - Add new feature for restoring many components at once. and many more. --- helper-scripts/backup_and_restore.sh | 665 +++++++++++++++++---------- 1 file changed, 416 insertions(+), 249 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 4cc5c78bee..0ec108bad9 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -2,15 +2,27 @@ # ----------------- Start Functions ----------------- +RED_COLOR="\e[31m" +GREEN_COLOR="\e[32m" +YELLOW_COLOR="\e[33m" +BLUE_COLOR="\e[34m" +BOLD="\e[1m" +ITALIC="\e[3m" +RESET="\e[0m" + function print_usage() { - echo "Usage: ${0} [option] [argument]" + echo -e "${BOLD}Usage:${RESET} ${0} [command] [command_options]${RESET}" + echo + echo -e "${BOLD}Commands:${RESET}" + echo -e "\t${GREEN_COLOR}-h, --help${RESET}\t\t\t${ITALIC}Print this help message${RESET}" + echo -e "\t${GREEN_COLOR}-b, --backup${RESET}\t${ITALIC}<${GREEN_COLOR}path${RESET}${ITALIC}>\t\tBackup one or more components of mailcow (example: ${0} -b /path/to/backup/folder -c vmail -c crypt)${RESET}" + echo -e "\t${GREEN_COLOR}-r, --restore${RESET}\t${ITALIC}<${GREEN_COLOR}path${RESET}${ITALIC}>\t\tRestore the full mailcow configuration from a backup${RESET}" + echo -e "\t${GREEN_COLOR}-d, --delete${RESET}\t${ITALIC}<${GREEN_COLOR}path${RESET}${ITALIC}> <${RESET}${GREEN_COLOR}days${RESET}${ITALIC}>\tDelete backups older than X days in the given path${RESET}" echo - echo "Options:" - echo -e " backup\t[crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days]" - echo -e " restore" + echo -e "${BOLD}Command Options:${RESET}" + echo -e "\t${GREEN_COLOR}-t, --threads${RESET}\t${ITALIC}<${GREEN_COLOR}num${RESET}${ITALIC}>\t\tSet the thread count for backup and restore operations${RESET}" + echo -e "\t${GREEN_COLOR}-c, --component${RESET}\t${ITALIC}<${GREEN_COLOR}component${RESET}${ITALIC}>\tSet the component(s) to backup or restore [vmail, crypt, redis, rspamd, postfix, mysql and all]${RESET}" echo - echo "Environment Variables:" - echo -e " THREADS\t\tYou can set the thread count with the THREADS environment variable before you run this script." } function check_required_tools() { @@ -75,69 +87,39 @@ function declare_restore_point() { fi } -# declare_file_selection declares the `FILE_SELECTION` -# globally which is the path to the backup folder inside +# declare_restore_components declares the `RESTORE_COMPONENTS` +# globally which is an array contains the components that should +# be restored from the given `RESTORE_POINT`. # `RESTORE_POINT` folder (which components to restore from). # -# If the function succeeds, the variable `FILE_SELECTION` +# If the function succeeds, the variable `RESTORE_COMPONENTS` # will be declared globally. # If the function fails, it will exit with a status of 1. -function declare_file_selection() { +function declare_restore_components() { local RESTORE_POINT="$1" local i=0 - local -A FILE_SELECTIONS - - echo "[ ${i} ] - all" + RESTORE_COMPONENTS=() # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz - FILE_SELECTIONS[0]=$(find "${RESTORE_POINT}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') - for file in $(ls -f "${RESTORE_POINT}"); do - if [[ ${file} =~ vmail ]]; then - ((i++)) - echo "[ ${i} ] - Mail directory (/var/vmail)" - FILE_SELECTIONS[${i}]="vmail" - elif [[ ${file} =~ crypt ]]; then - ((i++)) - echo "[ ${i} ] - Crypt data" - FILE_SELECTIONS[${i}]="crypt" - elif [[ ${file} =~ redis ]]; then - ((i++)) - echo "[ ${i} ] - Redis DB" - FILE_SELECTIONS[${i}]="redis" - elif [[ ${file} =~ rspamd ]]; then - if [[ $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then - ((i++)) - echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)" - FILE_SELECTIONS[${i}]="rspamd" - elif [[ $ARCH != $(find "${RESTORE_POINT}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then - echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m" - else - ((i++)) - echo "[ ${i} ] - Rspamd data" - FILE_SELECTIONS[${i}]="rspamd" - fi - elif [[ ${file} =~ postfix ]]; then - ((i++)) - echo "[ ${i} ] - Postfix data" - FILE_SELECTIONS[${i}]="postfix" - elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then - ((i++)) - echo "[ ${i} ] - SQL DB" - FILE_SELECTIONS[${i}]="mysql" + for file in $(find "${RESTORE_POINT}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//'); do + if [[ " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " ${file} " ]] || [[ " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " all " ]]; then + RESTORE_COMPONENTS+=("${file}") fi done echo - # Prompt the user to choose what to restore. - input_sel=-1 - while [[ ! "${input_sel}" =~ ^[0-9]+$ ]] || [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do - read -p "Select a dataset to restore: " input_sel + # Print the available files to restore + echo -e "\e[32mMatching available components to restore:\e[0m" + + local i=0 + for component in "${RESTORE_COMPONENTS[@]}"; do + ((i++)) + echo "[ ${i} ] - ${component}" done - # Declare the global variable - FILE_SELECTION="${FILE_SELECTIONS[${input_sel}]}" + echo } # restore_docker_component restores components used in @@ -171,6 +153,13 @@ function restore_docker_component() { exit 1 fi + if [[ ! -f "${RESTORE_LOCATION}/backup_${COMPONENT_NAME}.tar.gz" ]]; then + echo + echo -e "\e[33mWarning: ${RESTORE_LOCATION} does not contains a backup for [${COMPONENT_NAME}]!\e[0m" >&2 + echo -e "\e[33mSkipping restore for [${COMPONENT_NAME}]...\e[0m" >&2 + return 1 + fi + # Restoring Process docker stop "${CONTAINER_ID}" @@ -178,11 +167,84 @@ function restore_docker_component() { docker run -i --name mailcow-backup --rm \ -v "${RESTORE_LOCATION}:/backup:z" \ -v "${DOCKER_VOLUME_NAME}:/${COMPONENT_NAME}:z" \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_"${COMPONENT_NAME}".tar.gz + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${MAILCOW_BACKUP_RESTORE_THREADS}" -Pxvf /backup/backup_"${COMPONENT_NAME}".tar.gz docker start "${CONTAINER_ID}" } +function check_valid_backup_directory() { + BACKUP_LOCATION="${1}" + + if [[ -z "${BACKUP_LOCATION}" ]]; then + echo + echo -e "\e[31mFatal Error: Backup location not specified!\e[0m" + exit 1 + fi + + if [[ ! -e "${BACKUP_LOCATION}" ]]; then + echo -e "\e[33m${BACKUP_LOCATION} is not exist\e[0m" + read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION + if ! [[ ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then + echo -e "\e[31mexiting...\e[0m" + exit 0 + fi + + mkdir -p "${BACKUP_LOCATION}" + chmod 755 "${BACKUP_LOCATION}" + if [[ ! "${?}" -eq 0 ]]; then + echo -e "\e[31mFailed, check the error above!\e[0m" + exit 1 + fi + fi + + if [[ -d "${BACKUP_LOCATION}" ]]; then + if [[ -z $(echo $(stat -Lc %a "${BACKUP_LOCATION}") | grep -oE '[0-9][0-9][5-7]') ]]; then + echo -e "\e[31m${BACKUP_LOCATION} is not writable!\e[0m" + echo -e "\e[33mTry: chmod 755 ${BACKUP_LOCATION}\e[0m" + exit 1 + fi + else + echo -e "\e[31m${BACKUP_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[" + exit 1 + fi +} + +function check_valid_restore_directory() { + RESTORE_LOCATION="${1}" + + if [[ -z "${RESTORE_LOCATION}" ]]; then + echo + echo -e "\e[31mFatal Error: restore location not specified!\e[0m" + exit 1 + fi + + if [[ ! -e "${RESTORE_LOCATION}" ]]; then + echo -e "\e[31m${RESTORE_LOCATION} is not exist\e[0m" + exit 1 + fi + + if [[ ! -d "${RESTORE_LOCATION}" ]]; then + echo -e "\e[31m${RESTORE_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[0m" + exit 1 + fi +} + +function delete_old_backups() { + if [[ -z $(find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24))) ]]; then + echo -e "\e[33mNo backups to delete found.\e[0m" + exit 0 + fi + + echo -e "\e[34mBackups scheduled for deletion:\e[0m" + find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec printf "\e[33m- %s\e[0m\n" {} \; + read -p "$(echo -e "\e[1m\e[31mAre you sure you want to delete the above backups? type YES in capital letters to delete, else to skip: \e[0m")" DELETE_CONFIRM + if [[ "${DELETE_CONFIRM}" == "YES" ]]; then + find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec rm -rvf {} \; + else + echo -e "\e[33mOK, skipped.\e[0m" + fi +} + # ----------------- End Functions ----------------- check_required_tools @@ -195,95 +257,163 @@ if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" fi -if [[ ! ${1} =~ (backup|restore) ]]; then +if [[ $# -eq 0 ]]; then print_usage - exit 1 + exit 0 fi -if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days) ]]; then - if [[ -z "${2}" ]]; then - echo -e "\e[31mRequired argument for backup option\e[0m\n" - else - echo -e "\e[31mUnknown argument: ${2}\e[0m\n" - fi +MAILCOW_BACKUP_COMPONENTS=() - print_usage - exit 1 -fi +# These variables can be set using flags or environment variables +# Such as `./script.sh -b /path/to/backup` +# Or `MAILCOW_BACKUP_LOCATION=/path/to/backup ./script.sh` +MAILCOW_BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION:-}" +MAILCOW_RESTORE_LOCATION="${MAILCOW_RESTORE_LOCATION:-}" +MAILCOW_DELETE_LOCATION="${MAILCOW_DELETE_LOCATION:-}" +MAILCOW_DELETE_DAYS="${MAILCOW_DELETE_DAYS:-}" +MAILCOW_BACKUP_RESTORE_THREADS=${MAILCOW_BACKUP_RESTORE_THREADS:-1} -if [[ -z "${BACKUP_LOCATION}" ]]; then - while [[ -z "${BACKUP_LOCATION}" ]]; do - read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION - done +# Check for required files +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml +ENV_FILE=${SCRIPT_DIR}/../.env +ARCH=$(uname -m) + +if [ ! -f ${COMPOSE_FILE} ]; then + echo -e "\e[31mCompose file not found\e[0m" + exit 1 fi -if [[ ! "${BACKUP_LOCATION}" =~ ^/ ]]; then - echo -e "\e[31mBackup directory needs to be given as absolute path (starting with /).\e[0m" +if [ ! -f ${ENV_FILE} ]; then + echo -e "\e[31mEnvironment file not found\e[0m" exit 1 fi -# if "${BACKUP_LOCATION}" not exists, create it. -if [[ ! -e "${BACKUP_LOCATION}" ]]; then - if [[ "${1}" == "restore" ]]; then - echo -e "\e[31m${BACKUP_LOCATION} is not exists\e[0m" - exit 1 - fi +# Parse arguments +while [[ $# -gt 0 ]]; do + case "${1}" in + -h|--help) + print_usage + exit 0 + ;; + -b|--backup) + if ! [[ $# -gt 1 ]]; then + echo -e "\e[31mInvalid Option: -b/--backup requires an argument\e[0m" >&2 + exit 1 + fi - echo -e "\e[33m${BACKUP_LOCATION} is not exist\e[0m" - read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION - if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then - echo -e "\e[33mExiting without creating the backup location.\e[0m" - exit 0 - else - mkdir -p "${BACKUP_LOCATION}" - chmod 755 "${BACKUP_LOCATION}" + if ! [[ "${2}" =~ ^/ ]]; then + echo -e "\e[31mInvalid Option: -b/--backup requires an absolute path\e[0m" >&2 + exit 1 + fi - if [[ ! "${?}" -eq 0 ]]; then - echo -e "\e[31mFailed, check the error above!\e[0m" + MAILCOW_BACKUP_LOCATION=$(echo "${2}" | sed 's#/$##') + shift 2 + ;; + -r|--restore) + if ! [[ $# -gt 1 ]]; then + echo -e "\e[31mInvalid Option: -r/--restore requires an argument\e[0m" >&2 + exit 1 + fi + + if ! [[ "${2}" =~ ^/ ]]; then + echo -e "\e[31mInvalid Option: -r/--restore requires an absolute path\e[0m" >&2 + exit 1 + fi + + MAILCOW_RESTORE_LOCATION=$(echo "${2}" | sed 's#/$##') + shift 2 + ;; + -d|--delete-days|--delete) + if ! [[ $# -gt 2 ]]; then + echo -e "\e[31mInvalid Option: -d/--delete-days requires \e[0m" >&2 + exit 1 + fi + + if ! [[ "${2}" =~ ^/ ]]; then + echo -e "\e[31mInvalid Option: -d/--delete-days requires an absolute path\e[0m" >&2 + exit 1 + fi + + if ! [[ "${3}" =~ ^[0-9]+$ ]]; then + echo -e "\e[31mInvalid Option: -d/--delete-days requires a number\e[0m" >&2 + exit 1 + fi + + MAILCOW_DELETE_LOCATION=$(echo "${2}" | sed 's#/$##') + MAILCOW_DELETE_DAYS="${3}" + shift 3 + ;; + -c|--component) + if ! [[ $# -gt 1 ]]; then + echo -e "\e[31mInvalid Option: -c/--component requires an argument\e[0m" >&2 + exit 1 + fi + + if ! [[ "${2}" =~ ^(crypt|vmail|redis|rspamd|postfix|mysql|all)$ ]]; then + echo -e "\e[31mInvalid Option: -c/--component requires one of the following: crypt|vmail|redis|rspamd|postfix|mysql|all\e[0m" >&2 + exit 1 + fi + + # Do not allow duplicate components + if [[ ! " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " ${2} " ]]; then + MAILCOW_BACKUP_COMPONENTS+=("${2}") + fi + shift 2 + ;; + -t|--threads) + if ! [[ $# -gt 1 ]]; then + echo -e "\e[31mInvalid Option: -t/--threads requires an argument\e[0m" >&2 + exit 1 + fi + + if ! [[ "${2}" =~ ^[1-9][0-9]*$ ]]; then + echo -e "\e[31mInvalid Option: -t/--threads requires a positive number\e[0m" >&2 + exit 1 + fi + + echo -e "\e[32mUsing ${THREADS} thread(s) for this run.\e[0m" + MAILCOW_BACKUP_RESTORE_THREADS="${2}" + shift 2 + ;; + --) + shift + break + ;; + *) + echo -e "${RED_COLOR}Invalid Option: ${1}${RESET}" >&2 exit 1 - fi - fi -# if "${BACKUP_LOCATION}" is an exists directory, -# then just check the permissions -elif [[ -d "${BACKUP_LOCATION}" ]]; then - echo -e "\e[32mFound directory ${BACKUP_LOCATION}\e[0m" - if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a "${BACKUP_LOCATION}") | grep -oE '[0-9][0-9][5-7]') ]]; then - echo -e "\e[31m${BACKUP_LOCATION} is not write-able for others, that's required for a backup.\e[0m" - echo "Execute \`chmod 755 ${BACKUP_LOCATION}\` and try again." - exit 1 - fi -# else, the "${BACKUP_LOCATION}" is something else! an alien? yes! -else - echo -e "\e[31m${BACKUP_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[" - exit 1 -fi + ;; + esac +done -BACKUP_LOCATION=$(echo "${BACKUP_LOCATION}" | sed 's#/$##') -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml -ENV_FILE=${SCRIPT_DIR}/../.env -THREADS=$(echo ${THREADS:-1}) -ARCH=$(uname -m) +# Prevent passing --restore , --backup or --delete together +declare -i OPTION_COUNT=0 -if [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]]; then - echo -e "\e[32mUsing ${THREADS} thread(s) for this run.\e[0m" -else - echo -e "\e[31mThread input is not a number!\e[0m" - exit 1 +if [[ ! -z "${MAILCOW_DELETE_LOCATION}" ]]; then + OPTION_COUNT=$((OPTION_COUNT+1)) fi -if [ ! -f ${COMPOSE_FILE} ]; then - echo -e "\e[31mCompose file not found\e[0m" - exit 1 +if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then + OPTION_COUNT=$((OPTION_COUNT+1)) fi -if [ ! -f ${ENV_FILE} ]; then - echo -e "\e[31mEnvironment file not found\e[0m" +if [[ ! -z "${MAILCOW_RESTORE_LOCATION}" ]]; then + OPTION_COUNT=$((OPTION_COUNT+1)) +fi + +if [[ "${OPTION_COUNT}" -gt 1 ]] || [[ "${OPTION_COUNT}" -eq 0 ]]; then + echo -e "\e[31mYou should pass one of the following options: \e[33m-b/--backup\e[0m, \e[33m-r/--restore\e[0m or \e[33m-d/--delete\e[0m" exit 1 fi -echo -e "\e[33mUsing ${BACKUP_LOCATION} as backup/restore location.\e[0m" -echo +# Merge backup components if all is passed +if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]] || [[ ! -z "${MAILCOW_RESTORE_LOCATION}" ]]; then + if [[ " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " all " ]]; then + # MAILCOW_BACKUP_COMPONENTS=("crypt" "vmail" "redis" "rspamd" "postfix" "mysql") + MAILCOW_BACKUP_COMPONENTS=("all") + fi +fi source ${SCRIPT_DIR}/../mailcow.conf @@ -298,43 +428,45 @@ fi function backup() { DATE=$(date +"%Y-%m-%d-%H-%M-%S") - mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}" - chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" - cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" - touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" + mkdir -p "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}" + chmod 755 "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}" + cp "${SCRIPT_DIR}/../mailcow.conf" "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}" + touch "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" + + echo -e "\e[32mUsing ${MAILCOW_BACKUP_RESTORE_THREADS} thread(s) for this backup.\e[0m" while (( "$#" )); do case "$1" in vmail|all) docker run --name mailcow-backup --rm \ - -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ + -v "${MAILCOW_BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${MAILCOW_BACKUP_RESTORE_THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail ;;& crypt|all) docker run --name mailcow-backup --rm \ -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${MAILCOW_BACKUP_RESTORE_THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt ;;& redis|all) docker exec $(docker ps -qf name=redis-mailcow) redis-cli save docker run --name mailcow-backup --rm \ -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${MAILCOW_BACKUP_RESTORE_THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis ;;& rspamd|all) docker run --name mailcow-backup --rm \ -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${MAILCOW_BACKUP_RESTORE_THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd ;;& postfix|all) docker run --name mailcow-backup --rm \ -v "${BACKUP_LOCATION}"/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${MAILCOW_BACKUP_RESTORE_THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix ;;& mysql|all) SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) @@ -356,14 +488,6 @@ function backup() { /bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;" fi ;;& - --delete-days) - shift - if [[ "${1}" =~ ^[0-9]+$ ]]; then - find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \; - else - echo -e "\e[31mParameter of --delete-days is not a number.\e[0m" - fi - ;; esac shift done @@ -371,11 +495,9 @@ function backup() { function restore() { if [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then - COMPOSE_COMMAND="docker compose" - + COMPOSE_COMMAND="docker compose" elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then COMPOSE_COMMAND="docker-compose" - else echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m" exit 1 @@ -387,109 +509,112 @@ function restore() { echo RESTORE_LOCATION="${1}" shift - while (( "$#" )); do - case "$1" in - vmail) - restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "dovecot-mailcow" "vmail" - - echo - echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" - echo - echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'" - echo - read -p "Force a resync now? [y|N] " FORCE_RESYNC - if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then - docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' - else - echo "OK, skipped." - fi - ;; - redis) - restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "redis-mailcow" "redis" - ;; - crypt) - restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "dovecot-mailcow" "crypt" - ;; - rspamd) - if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then - echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" - sleep 2 - echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" - sleep 2 - restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" - - elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then - echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." - sleep 2 - echo -e "Skipping rspamd due to compatibility issues!\e[0m" - else - restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" - fi - ;; - postfix) - restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "postfix-mailcow" "postfix" - ;; - mysql|mariadb) - SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) - if [[ -z "${SQLIMAGE}" ]]; then - echo -e "\e[31mCould not determine SQL image version, skipping restore...\e[0m" - shift - continue - elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then - echo -e "\e[31mCould not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore.\e[0m" - echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process." - shift - continue - else - read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW - if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then + for component in ${RESTORE_COMPONENTS[@]}; do + echo + echo -e "\n\e[32mRestoring ${component}...\e[0m" + sleep 1 + case ${component} in + vmail) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "dovecot-mailcow" "vmail" + + echo + echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" + echo + echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'" + echo + read -p "Force a resync now? [y|N] " FORCE_RESYNC + if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then + docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' + else echo "OK, skipped." + fi + ;; + redis) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "redis-mailcow" "redis" + ;; + crypt) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "dovecot-mailcow" "crypt" + ;; + rspamd) + if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" + sleep 2 + echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" + sleep 2 + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" + + elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then + echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." + sleep 2 + echo -e "Skipping rspamd due to compatibility issues!\e[0m" + else + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" + fi + ;; + postfix) + restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "postfix-mailcow" "postfix" + ;; + mysql|mariadb) + SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) + if [[ -z "${SQLIMAGE}" ]]; then + echo -e "\e[31mCould not determine SQL image version, skipping restore...\e[0m" + shift + continue + elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then + echo -e "\e[31mCould not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore.\e[0m" + echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process." shift continue else - echo "Stopping mailcow..." - ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down + read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW + if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then + echo "OK, skipped." + shift + continue + else + echo "Stopping mailcow..." + ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down + fi + #docker stop $(docker ps -qf name=mysql-mailcow) + if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then + docker run --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:rw,z \ + --entrypoint= \ + -v ${RESTORE_LOCATION}/mysql:/backup:z \ + ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/" + elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then + docker run \ + -i --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:z \ + --entrypoint= \ + -u mysql \ + -v ${RESTORE_LOCATION}:/backup:z \ + ${SQLIMAGE} /bin/sh -c "mysqld --skip-grant-tables & \ + until mysqladmin ping; do sleep 3; done && \ + echo Restoring... && \ + gunzip < backup/backup_mysql.gz | mysql -uroot && \ + mysql -uroot -e SHUTDOWN;" + elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then + docker run --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ + --entrypoint= \ + -v ${RESTORE_LOCATION}:/backup:z \ + ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ + /bin/rm -rf /backup_mariadb/* ; \ + /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz" + fi + echo "Modifying mailcow.conf..." + source ${RESTORE_LOCATION}/mailcow.conf + sed -i --follow-symlinks "/DBNAME/c\DBNAME=${DBNAME}" ${SCRIPT_DIR}/../mailcow.conf + sed -i --follow-symlinks "/DBUSER/c\DBUSER=${DBUSER}" ${SCRIPT_DIR}/../mailcow.conf + sed -i --follow-symlinks "/DBPASS/c\DBPASS=${DBPASS}" ${SCRIPT_DIR}/../mailcow.conf + sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf + source ${SCRIPT_DIR}/../mailcow.conf + echo "Starting mailcow..." + ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d + #docker start $(docker ps -aqf name=mysql-mailcow) fi - #docker stop $(docker ps -qf name=mysql-mailcow) - if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then - docker run --name mailcow-backup --rm \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:rw,z \ - --entrypoint= \ - -v ${RESTORE_LOCATION}/mysql:/backup:z \ - ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/" - elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then - docker run \ - -i --name mailcow-backup --rm \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:z \ - --entrypoint= \ - -u mysql \ - -v ${RESTORE_LOCATION}:/backup:z \ - ${SQLIMAGE} /bin/sh -c "mysqld --skip-grant-tables & \ - until mysqladmin ping; do sleep 3; done && \ - echo Restoring... && \ - gunzip < backup/backup_mysql.gz | mysql -uroot && \ - mysql -uroot -e SHUTDOWN;" - elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then - docker run --name mailcow-backup --rm \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ - --entrypoint= \ - -v ${RESTORE_LOCATION}:/backup:z \ - ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ - /bin/rm -rf /backup_mariadb/* ; \ - /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz" - fi - echo "Modifying mailcow.conf..." - source ${RESTORE_LOCATION}/mailcow.conf - sed -i --follow-symlinks "/DBNAME/c\DBNAME=${DBNAME}" ${SCRIPT_DIR}/../mailcow.conf - sed -i --follow-symlinks "/DBUSER/c\DBUSER=${DBUSER}" ${SCRIPT_DIR}/../mailcow.conf - sed -i --follow-symlinks "/DBPASS/c\DBPASS=${DBPASS}" ${SCRIPT_DIR}/../mailcow.conf - sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf - source ${SCRIPT_DIR}/../mailcow.conf - echo "Starting mailcow..." - ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d - #docker start $(docker ps -aqf name=mysql-mailcow) - fi - ;; + ;; esac shift done @@ -498,17 +623,59 @@ function restore() { docker start $(docker ps -aqf name=watchdog-mailcow) } -if [[ ${1} == "backup" ]]; then - backup ${@,,} -elif [[ ${1} == "restore" ]]; then +# +# Backup Process +# +if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then + + check_valid_backup_directory "${MAILCOW_BACKUP_LOCATION}" + + echo -e "\e[32mUsing ${MAILCOW_BACKUP_LOCATION} as backup location...\e[0m" + + # If you didn't specify any backup components, then exit + if [[ ${#MAILCOW_BACKUP_COMPONENTS[@]} -eq 0 ]]; then + echo -e "\e[31mNo components specified for the backup, please see --help\e[0m" + exit 1 + fi + + backup "${MAILCOW_BACKUP_COMPONENTS[@]}" +fi + +# +# Restore Process +# +if [[ ! -z "${MAILCOW_RESTORE_LOCATION}" ]]; then + + check_valid_restore_directory "${MAILCOW_RESTORE_LOCATION}" + + # If you didn't specify any backup components for the restore, then exit + if [[ ${#MAILCOW_BACKUP_COMPONENTS[@]} -eq 0 ]]; then + echo -e "\e[31mNo components specified for the restore, please see --help\e[0m" + exit 1 + fi + + echo -e "\e[32mUsing ${MAILCOW_RESTORE_LOCATION} as restore location...\e[0m" + # Calling `declare_restore_point` will # declare `RESTORE_POINT` globally. - declare_restore_point "${BACKUP_LOCATION}" + declare_restore_point "${MAILCOW_RESTORE_LOCATION}" - # Calling `declare_file_selection` will - # declare `FILE_SELECTION` globally. - declare_file_selection "${RESTORE_POINT}" + # Calling `declare_restore_components` will + # declare `RESTORE_COMPONENTS` globally. + declare_restore_components "${RESTORE_POINT}" + + echo -e "\n\e[32mRestoring will start in \e[1m5 seconds\e[0m. Press \e[1mCtrl+C\e[0m to stop.\n\e[0m" + sleep 5 + + echo "Restoring ${MAILCOW_BACKUP_COMPONENTS[*]} from ${RESTORE_POINT}..." + restore "${RESTORE_POINT}" +fi + +# +# Delete Process +# +if [[ ! -z "${MAILCOW_DELETE_LOCATION}" ]]; then + echo "Deleting backups older than ${MAILCOW_DELETE_DAYS} days in ${MAILCOW_DELETE_LOCATION}" - echo "Restoring ${FILE_SELECTION} from ${RESTORE_POINT}..." - restore "${RESTORE_POINT}" "${FILE_SELECTION}" + delete_old_backups fi From 92118b4166ee5076c2985aa887cd0285c089d5f8 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Fri, 23 Aug 2024 01:56:35 +0300 Subject: [PATCH 21/31] Use named colors, instead of ANSI codes --- helper-scripts/backup_and_restore.sh | 114 +++++++++++++-------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 0ec108bad9..af5d91ee9f 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -31,13 +31,13 @@ function check_required_tools() { for bin in "${required_tools[@]}"; do if [[ -z $(which ${bin}) ]]; then - echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + echo -e "${RED_COLOR}Cannot find ${bin} in local PATH, exiting...${RESET}" exit 1 fi done if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then - echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" + echo -e "${RED_COLOR}BusyBox grep detected on local system, please install GNU grep${RESET}" exit 1 fi } @@ -53,7 +53,7 @@ function declare_restore_point() { # Check subfolders inside BACKUP_LOCATION if [[ $(find "${BACKUP_LOCATION}"/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then - echo -e "\e[31mSelected backup location has no subfolders\e[0m" + echo -e "${RED_COLOR}Selected backup location has no subfolders${RESET}" exit 1 fi @@ -82,7 +82,7 @@ function declare_restore_point() { # to be used outside the function. RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then - echo -e "\e[31mNo datasets found\e[0m" + echo -e "${RED_COLOR}No datasets found${RESET}" exit 1 fi } @@ -111,7 +111,7 @@ function declare_restore_components() { echo # Print the available files to restore - echo -e "\e[32mMatching available components to restore:\e[0m" + echo -e "${GREEN_COLOR}Matching available components to restore:${RESET}" local i=0 for component in "${RESTORE_COMPONENTS[@]}"; do @@ -149,14 +149,14 @@ function restore_docker_component() { if [[ -z "${DOCKER_VOLUME_NAME}" ]]; then echo - echo -e "\e[31mFatal Error: Docker volume for [${DOCKER_COMPOSE_PROJECT_NAME}_${DOCKER_IMAGE_NAME}] not found!\e[0m" + echo -e "${RED_COLOR}Fatal Error: Docker volume for [${DOCKER_COMPOSE_PROJECT_NAME}_${DOCKER_IMAGE_NAME}] not found!${RESET}" exit 1 fi if [[ ! -f "${RESTORE_LOCATION}/backup_${COMPONENT_NAME}.tar.gz" ]]; then echo - echo -e "\e[33mWarning: ${RESTORE_LOCATION} does not contains a backup for [${COMPONENT_NAME}]!\e[0m" >&2 - echo -e "\e[33mSkipping restore for [${COMPONENT_NAME}]...\e[0m" >&2 + echo -e "${YELLOW_COLOR}Warning: ${RESTORE_LOCATION} does not contains a backup for [${COMPONENT_NAME}]!${RESET}" >&2 + echo -e "${YELLOW_COLOR}Skipping restore for [${COMPONENT_NAME}]...${RESET}" >&2 return 1 fi @@ -177,34 +177,34 @@ function check_valid_backup_directory() { if [[ -z "${BACKUP_LOCATION}" ]]; then echo - echo -e "\e[31mFatal Error: Backup location not specified!\e[0m" + echo -e "${RED_COLOR}Fatal Error: Backup location not specified!${RESET}" exit 1 fi if [[ ! -e "${BACKUP_LOCATION}" ]]; then - echo -e "\e[33m${BACKUP_LOCATION} is not exist\e[0m" + echo -e "${YELLOW_COLOR}${BACKUP_LOCATION} is not exist${RESET}" read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION if ! [[ ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then - echo -e "\e[31mexiting...\e[0m" + echo -e "${RED_COLOR}exiting...${RESET}" exit 0 fi mkdir -p "${BACKUP_LOCATION}" chmod 755 "${BACKUP_LOCATION}" if [[ ! "${?}" -eq 0 ]]; then - echo -e "\e[31mFailed, check the error above!\e[0m" + echo -e "${RED_COLOR}Failed, check the error above!${RESET}" exit 1 fi fi if [[ -d "${BACKUP_LOCATION}" ]]; then if [[ -z $(echo $(stat -Lc %a "${BACKUP_LOCATION}") | grep -oE '[0-9][0-9][5-7]') ]]; then - echo -e "\e[31m${BACKUP_LOCATION} is not writable!\e[0m" - echo -e "\e[33mTry: chmod 755 ${BACKUP_LOCATION}\e[0m" + echo -e "${RED_COLOR}${BACKUP_LOCATION} is not writable!${RESET}" + echo -e "${YELLOW_COLOR}Try: chmod 755 ${BACKUP_LOCATION}${RESET}" exit 1 fi else - echo -e "\e[31m${BACKUP_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[" + echo -e "${RED_COLOR}${BACKUP_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[" exit 1 fi } @@ -214,34 +214,34 @@ function check_valid_restore_directory() { if [[ -z "${RESTORE_LOCATION}" ]]; then echo - echo -e "\e[31mFatal Error: restore location not specified!\e[0m" + echo -e "${RED_COLOR}Fatal Error: restore location not specified!${RESET}" exit 1 fi if [[ ! -e "${RESTORE_LOCATION}" ]]; then - echo -e "\e[31m${RESTORE_LOCATION} is not exist\e[0m" + echo -e "${RED_COLOR}${RESTORE_LOCATION} is not exist${RESET}" exit 1 fi if [[ ! -d "${RESTORE_LOCATION}" ]]; then - echo -e "\e[31m${RESTORE_LOCATION} is not a valid path! Maybe a file or a symbolic?\e[0m" + echo -e "${RED_COLOR}${RESTORE_LOCATION} is not a valid path! Maybe a file or a symbolic?${RESET}" exit 1 fi } function delete_old_backups() { if [[ -z $(find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24))) ]]; then - echo -e "\e[33mNo backups to delete found.\e[0m" + echo -e "${YELLOW_COLOR}No backups to delete found.${RESET}" exit 0 fi - echo -e "\e[34mBackups scheduled for deletion:\e[0m" - find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec printf "\e[33m- %s\e[0m\n" {} \; - read -p "$(echo -e "\e[1m\e[31mAre you sure you want to delete the above backups? type YES in capital letters to delete, else to skip: \e[0m")" DELETE_CONFIRM + echo -e "${BLUE_COLOR}Backups scheduled for deletion:${RESET}" + find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec printf "${YELLOW_COLOR}- %s${RESET}\n" {} \; + read -p "$(echo -e "${BOLD}${RED_COLOR}Are you sure you want to delete the above backups? type YES in capital letters to delete, else to skip: ${RESET}")" DELETE_CONFIRM if [[ "${DELETE_CONFIRM}" == "YES" ]]; then find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec rm -rvf {} \; else - echo -e "\e[33mOK, skipped.\e[0m" + echo -e "${YELLOW_COLOR}OK, skipped.${RESET}" fi } @@ -280,12 +280,12 @@ ENV_FILE=${SCRIPT_DIR}/../.env ARCH=$(uname -m) if [ ! -f ${COMPOSE_FILE} ]; then - echo -e "\e[31mCompose file not found\e[0m" + echo -e "${RED_COLOR}Compose file not found${RESET}" exit 1 fi if [ ! -f ${ENV_FILE} ]; then - echo -e "\e[31mEnvironment file not found\e[0m" + echo -e "${RED_COLOR}Environment file not found${RESET}" exit 1 fi @@ -298,12 +298,12 @@ while [[ $# -gt 0 ]]; do ;; -b|--backup) if ! [[ $# -gt 1 ]]; then - echo -e "\e[31mInvalid Option: -b/--backup requires an argument\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -b/--backup requires an argument${RESET}" >&2 exit 1 fi if ! [[ "${2}" =~ ^/ ]]; then - echo -e "\e[31mInvalid Option: -b/--backup requires an absolute path\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -b/--backup requires an absolute path${RESET}" >&2 exit 1 fi @@ -312,12 +312,12 @@ while [[ $# -gt 0 ]]; do ;; -r|--restore) if ! [[ $# -gt 1 ]]; then - echo -e "\e[31mInvalid Option: -r/--restore requires an argument\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -r/--restore requires an argument${RESET}" >&2 exit 1 fi if ! [[ "${2}" =~ ^/ ]]; then - echo -e "\e[31mInvalid Option: -r/--restore requires an absolute path\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -r/--restore requires an absolute path${RESET}" >&2 exit 1 fi @@ -326,17 +326,17 @@ while [[ $# -gt 0 ]]; do ;; -d|--delete-days|--delete) if ! [[ $# -gt 2 ]]; then - echo -e "\e[31mInvalid Option: -d/--delete-days requires \e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -d/--delete-days requires ${RESET}" >&2 exit 1 fi if ! [[ "${2}" =~ ^/ ]]; then - echo -e "\e[31mInvalid Option: -d/--delete-days requires an absolute path\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -d/--delete-days requires an absolute path${RESET}" >&2 exit 1 fi if ! [[ "${3}" =~ ^[0-9]+$ ]]; then - echo -e "\e[31mInvalid Option: -d/--delete-days requires a number\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -d/--delete-days requires a number${RESET}" >&2 exit 1 fi @@ -346,12 +346,12 @@ while [[ $# -gt 0 ]]; do ;; -c|--component) if ! [[ $# -gt 1 ]]; then - echo -e "\e[31mInvalid Option: -c/--component requires an argument\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -c/--component requires an argument${RESET}" >&2 exit 1 fi if ! [[ "${2}" =~ ^(crypt|vmail|redis|rspamd|postfix|mysql|all)$ ]]; then - echo -e "\e[31mInvalid Option: -c/--component requires one of the following: crypt|vmail|redis|rspamd|postfix|mysql|all\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -c/--component requires one of the following: crypt|vmail|redis|rspamd|postfix|mysql|all${RESET}" >&2 exit 1 fi @@ -363,16 +363,16 @@ while [[ $# -gt 0 ]]; do ;; -t|--threads) if ! [[ $# -gt 1 ]]; then - echo -e "\e[31mInvalid Option: -t/--threads requires an argument\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -t/--threads requires an argument${RESET}" >&2 exit 1 fi if ! [[ "${2}" =~ ^[1-9][0-9]*$ ]]; then - echo -e "\e[31mInvalid Option: -t/--threads requires a positive number\e[0m" >&2 + echo -e "${RED_COLOR}Invalid Option: -t/--threads requires a positive number${RESET}" >&2 exit 1 fi - echo -e "\e[32mUsing ${THREADS} thread(s) for this run.\e[0m" + echo -e "${GREEN_COLOR}Using ${THREADS} thread(s) for this run.${RESET}" MAILCOW_BACKUP_RESTORE_THREADS="${2}" shift 2 ;; @@ -403,7 +403,7 @@ if [[ ! -z "${MAILCOW_RESTORE_LOCATION}" ]]; then fi if [[ "${OPTION_COUNT}" -gt 1 ]] || [[ "${OPTION_COUNT}" -eq 0 ]]; then - echo -e "\e[31mYou should pass one of the following options: \e[33m-b/--backup\e[0m, \e[33m-r/--restore\e[0m or \e[33m-d/--delete\e[0m" + echo -e "${RED_COLOR}You should pass one of the following options: ${YELLOW_COLOR}-b/--backup${RESET}, ${YELLOW_COLOR}-r/--restore${RESET} or ${YELLOW_COLOR}-d/--delete${RESET}" exit 1 fi @@ -418,10 +418,10 @@ fi source ${SCRIPT_DIR}/../mailcow.conf if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then - echo -e "\e[31mCould not determine compose project name\e[0m" + echo -e "${RED_COLOR}Could not determine compose project name${RESET}" exit 1 else - echo -e "\e[32mFound project name ${COMPOSE_PROJECT_NAME}\e[0m" + echo -e "${GREEN_COLOR}Found project name ${COMPOSE_PROJECT_NAME}${RESET}" CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") fi @@ -433,7 +433,7 @@ function backup() { cp "${SCRIPT_DIR}/../mailcow.conf" "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}" touch "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" - echo -e "\e[32mUsing ${MAILCOW_BACKUP_RESTORE_THREADS} thread(s) for this backup.\e[0m" + echo -e "${GREEN_COLOR}Using ${MAILCOW_BACKUP_RESTORE_THREADS} thread(s) for this backup.${RESET}" while (( "$#" )); do case "$1" in @@ -471,11 +471,11 @@ function backup() { mysql|all) SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) if [[ -z "${SQLIMAGE}" ]]; then - echo -e "\e[31mCould not determine SQL image version, skipping backup...\e[0m" + echo -e "${RED_COLOR}Could not determine SQL image version, skipping backup...${RESET}" shift continue else - echo -e "\e[32mUsing SQL image ${SQLIMAGE}, starting...\e[0m" + echo -e "${GREEN_COLOR}Using SQL image ${SQLIMAGE}, starting...${RESET}" docker run --name mailcow-backup --rm \ --network $(docker network ls -qf name=^${CMPS_PRJ}_mailcow-network$) \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:ro,z \ @@ -499,19 +499,19 @@ function restore() { elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then COMPOSE_COMMAND="docker-compose" else - echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m" + echo -e "${RED_COLOR}Can not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...${RESET}" exit 1 fi echo - echo -e "\e[33mStopping watchdog-mailcow...\e[0m" + echo -e "${YELLOW_COLOR}Stopping watchdog-mailcow...${RESET}" docker stop $(docker ps -qf name=watchdog-mailcow) echo RESTORE_LOCATION="${1}" shift for component in ${RESTORE_COMPONENTS[@]}; do echo - echo -e "\n\e[32mRestoring ${component}...\e[0m" + echo -e "\n${GREEN_COLOR}Restoring ${component}...${RESET}" sleep 1 case ${component} in vmail) @@ -537,16 +537,16 @@ function restore() { ;; rspamd) if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then - echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" + echo -e "${YELLOW_COLOR}Could not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" sleep 2 - echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" + echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.${RESET}" sleep 2 restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then - echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." + echo -e "${RED_COLOR}The Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." sleep 2 - echo -e "Skipping rspamd due to compatibility issues!\e[0m" + echo -e "Skipping rspamd due to compatibility issues!${RESET}" else restore_docker_component "${RESTORE_LOCATION}" "${CMPS_PRJ}" "rspamd-mailcow" "rspamd" fi @@ -557,11 +557,11 @@ function restore() { mysql|mariadb) SQLIMAGE=$(grep -iEo '(mysql|mariadb):.+' ${COMPOSE_FILE}) if [[ -z "${SQLIMAGE}" ]]; then - echo -e "\e[31mCould not determine SQL image version, skipping restore...\e[0m" + echo -e "${RED_COLOR}Could not determine SQL image version, skipping restore...${RESET}" shift continue elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then - echo -e "\e[31mCould not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore.\e[0m" + echo -e "${RED_COLOR}Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore.${RESET}" echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process." shift continue @@ -630,11 +630,11 @@ if [[ ! -z "${MAILCOW_BACKUP_LOCATION}" ]]; then check_valid_backup_directory "${MAILCOW_BACKUP_LOCATION}" - echo -e "\e[32mUsing ${MAILCOW_BACKUP_LOCATION} as backup location...\e[0m" + echo -e "${GREEN_COLOR}Using ${MAILCOW_BACKUP_LOCATION} as backup location...${RESET}" # If you didn't specify any backup components, then exit if [[ ${#MAILCOW_BACKUP_COMPONENTS[@]} -eq 0 ]]; then - echo -e "\e[31mNo components specified for the backup, please see --help\e[0m" + echo -e "${RED_COLOR}No components specified for the backup, please see --help${RESET}" exit 1 fi @@ -650,11 +650,11 @@ if [[ ! -z "${MAILCOW_RESTORE_LOCATION}" ]]; then # If you didn't specify any backup components for the restore, then exit if [[ ${#MAILCOW_BACKUP_COMPONENTS[@]} -eq 0 ]]; then - echo -e "\e[31mNo components specified for the restore, please see --help\e[0m" + echo -e "${RED_COLOR}No components specified for the restore, please see --help${RESET}" exit 1 fi - echo -e "\e[32mUsing ${MAILCOW_RESTORE_LOCATION} as restore location...\e[0m" + echo -e "${GREEN_COLOR}Using ${MAILCOW_RESTORE_LOCATION} as restore location...${RESET}" # Calling `declare_restore_point` will # declare `RESTORE_POINT` globally. @@ -664,7 +664,7 @@ if [[ ! -z "${MAILCOW_RESTORE_LOCATION}" ]]; then # declare `RESTORE_COMPONENTS` globally. declare_restore_components "${RESTORE_POINT}" - echo -e "\n\e[32mRestoring will start in \e[1m5 seconds\e[0m. Press \e[1mCtrl+C\e[0m to stop.\n\e[0m" + echo -e "\n${GREEN_COLOR}Restoring will start in ${BOLD}5 seconds${RESET}. Press ${BOLD}Ctrl+C${RESET} to stop.\n${RESET}" sleep 5 echo "Restoring ${MAILCOW_BACKUP_COMPONENTS[*]} from ${RESTORE_POINT}..." From a50bf619e52cc2b5b0ed2104295936bf4155179e Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Fri, 23 Aug 2024 01:57:11 +0300 Subject: [PATCH 22/31] Fix: THREADS not printed in `backup_and_restore.sh` --- helper-scripts/backup_and_restore.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index af5d91ee9f..f83c384f5f 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -372,8 +372,9 @@ while [[ $# -gt 0 ]]; do exit 1 fi - echo -e "${GREEN_COLOR}Using ${THREADS} thread(s) for this run.${RESET}" MAILCOW_BACKUP_RESTORE_THREADS="${2}" + + echo -e "${GREEN_COLOR}Using ${MAILCOW_BACKUP_RESTORE_THREADS} thread(s) for this run.${RESET}" shift 2 ;; --) From 85aa1fb6c4b4ccf5a956d00faa7d2b36481028a6 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Fri, 23 Aug 2024 16:19:02 +0300 Subject: [PATCH 23/31] Fix: Double quote array expansions to avoid re-splitting elements --- helper-scripts/backup_and_restore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index f83c384f5f..96ce2df4b1 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -510,7 +510,7 @@ function restore() { echo RESTORE_LOCATION="${1}" shift - for component in ${RESTORE_COMPONENTS[@]}; do + for component in "${RESTORE_COMPONENTS[@]}"; do echo echo -e "\n${GREEN_COLOR}Restoring ${component}...${RESET}" sleep 1 From b7abccc313172db34083479b9cde19c091729a42 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 27 Aug 2024 10:38:39 +0300 Subject: [PATCH 24/31] Fix: mysql component not shown when trying to restore it --- helper-scripts/backup_and_restore.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 96ce2df4b1..a7506b80c3 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -101,6 +101,11 @@ function declare_restore_components() { local i=0 RESTORE_COMPONENTS=() + # Fix mysql component is stored as `backup_mariadb.tar.gz` + if [[ " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " mysql " ]]; then + MAILCOW_BACKUP_COMPONENTS+=("mariadb") + fi + # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz for file in $(find "${RESTORE_POINT}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//'); do if [[ " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " ${file} " ]] || [[ " ${MAILCOW_BACKUP_COMPONENTS[*]} " =~ " all " ]]; then From 9b4bae8c89de245e7a0377cb2b13de4be2ed184d Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 27 Aug 2024 10:41:03 +0300 Subject: [PATCH 25/31] Fix: Make sure there's components before restoring process --- helper-scripts/backup_and_restore.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index a7506b80c3..41b25c55d8 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -115,6 +115,12 @@ function declare_restore_components() { echo + # If no components were found, exit with error + if [[ ${#RESTORE_COMPONENTS[@]} -eq 0 ]]; then + echo -e "${RED_COLOR}No components found to restore${RESET}" + exit 1 + fi + # Print the available files to restore echo -e "${GREEN_COLOR}Matching available components to restore:${RESET}" From cd756429c9592c8cbec50dcac9c7ee819fa9c1e9 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 27 Aug 2024 10:53:35 +0300 Subject: [PATCH 26/31] Add `grep`, `find` and `sed` to the required_tools, also BusyBox check. --- helper-scripts/backup_and_restore.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 41b25c55d8..513cbd004d 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -27,7 +27,7 @@ function print_usage() { function check_required_tools() { # Add the required tools to the array - local required_tools=("docker") + local required_tools=("docker" "grep" "find" "sed") for bin in "${required_tools[@]}"; do if [[ -z $(which ${bin}) ]]; then @@ -40,6 +40,16 @@ function check_required_tools() { echo -e "${RED_COLOR}BusyBox grep detected on local system, please install GNU grep${RESET}" exit 1 fi + + if find --help 2>&1 | head -n 1 | grep -q -i "busybox"; then + echo -e "${RED_COLOR}BusyBox find detected on local system, please install GNU findutils${RESET}" + exit 1 + fi + + if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then + echo -e "${RED_COLOR}BusyBox sed detected on local system, please install GNU sed${RESET}" + exit 1 + fi } # declare_restore_point declares the `RESTORE_POINT` From 4cac270500adf9cba6636c427c29b8f36996fd08 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 27 Aug 2024 10:58:49 +0300 Subject: [PATCH 27/31] Remove duplicate threads notice for backup process --- helper-scripts/backup_and_restore.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 513cbd004d..76732a5671 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -455,8 +455,6 @@ function backup() { cp "${SCRIPT_DIR}/../mailcow.conf" "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}" touch "${MAILCOW_BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" - echo -e "${GREEN_COLOR}Using ${MAILCOW_BACKUP_RESTORE_THREADS} thread(s) for this backup.${RESET}" - while (( "$#" )); do case "$1" in vmail|all) From 48592ab2641aeb11e0d308f2d71ed79a736f9cd0 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 27 Aug 2024 18:05:09 +0300 Subject: [PATCH 28/31] Implement `--yes` flag, to automate `--delete` and `--backup` script --- helper-scripts/backup_and_restore.sh | 64 ++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 76732a5671..42aba3e8d7 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -22,6 +22,7 @@ function print_usage() { echo -e "${BOLD}Command Options:${RESET}" echo -e "\t${GREEN_COLOR}-t, --threads${RESET}\t${ITALIC}<${GREEN_COLOR}num${RESET}${ITALIC}>\t\tSet the thread count for backup and restore operations${RESET}" echo -e "\t${GREEN_COLOR}-c, --component${RESET}\t${ITALIC}<${GREEN_COLOR}component${RESET}${ITALIC}>\tSet the component(s) to backup or restore [vmail, crypt, redis, rspamd, postfix, mysql and all]${RESET}" + echo -e "\t${GREEN_COLOR}--yes${RESET}\t\t\t\t${ITALIC}Do not ask for confirmation, answer yes to all prompts (good for automation)${RESET}" echo } @@ -204,10 +205,13 @@ function check_valid_backup_directory() { if [[ ! -e "${BACKUP_LOCATION}" ]]; then echo -e "${YELLOW_COLOR}${BACKUP_LOCATION} is not exist${RESET}" - read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION - if ! [[ ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then - echo -e "${RED_COLOR}exiting...${RESET}" - exit 0 + + if [[ "${MAILCOW_YES_TO_ALL}" = "0" ]]; then + read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION + if ! [[ ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then + echo -e "${RED_COLOR}exiting...${RESET}" + exit 0 + fi fi mkdir -p "${BACKUP_LOCATION}" @@ -258,11 +262,18 @@ function delete_old_backups() { echo -e "${BLUE_COLOR}Backups scheduled for deletion:${RESET}" find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec printf "${YELLOW_COLOR}- %s${RESET}\n" {} \; - read -p "$(echo -e "${BOLD}${RED_COLOR}Are you sure you want to delete the above backups? type YES in capital letters to delete, else to skip: ${RESET}")" DELETE_CONFIRM - if [[ "${DELETE_CONFIRM}" == "YES" ]]; then + + if [[ "${MAILCOW_YES_TO_ALL}" = "1" ]]; then + echo -e "${GREEN_COLOR}Start deleting...${RESET}" find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec rm -rvf {} \; + echo -e "${GREEN_COLOR}Done.${RESET}" else - echo -e "${YELLOW_COLOR}OK, skipped.${RESET}" + read -p "$(echo -e "${BOLD}${RED_COLOR}Are you sure you want to delete the above backups? type YES in capital letters to delete, else to skip: ${RESET}")" DELETE_CONFIRM + if [[ "${DELETE_CONFIRM}" == "YES" ]]; then + find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) -exec rm -rvf {} \; + else + echo -e "${YELLOW_COLOR}OK, skipped.${RESET}" + fi fi } @@ -292,7 +303,8 @@ MAILCOW_BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION:-}" MAILCOW_RESTORE_LOCATION="${MAILCOW_RESTORE_LOCATION:-}" MAILCOW_DELETE_LOCATION="${MAILCOW_DELETE_LOCATION:-}" MAILCOW_DELETE_DAYS="${MAILCOW_DELETE_DAYS:-}" -MAILCOW_BACKUP_RESTORE_THREADS=${MAILCOW_BACKUP_RESTORE_THREADS:-1} +MAILCOW_BACKUP_RESTORE_THREADS="${MAILCOW_BACKUP_RESTORE_THREADS:-1}" +MAILCOW_YES_TO_ALL="${MAILCOW_YES_TO_ALL:-0}" # Check for required files SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -398,6 +410,10 @@ while [[ $# -gt 0 ]]; do echo -e "${GREEN_COLOR}Using ${MAILCOW_BACKUP_RESTORE_THREADS} thread(s) for this run.${RESET}" shift 2 ;; + --yes) + MAILCOW_YES_TO_ALL="1" + shift + ;; --) shift break @@ -542,11 +558,18 @@ function restore() { echo echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'" echo - read -p "Force a resync now? [y|N] " FORCE_RESYNC - if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then + + # Skip prompt if --yes flag is present + if [[ "${MAILCOW_YES_TO_ALL}" == "1" ]]; then + echo -e "${GREEN_COLOR}Forcing a resync now due to --yes flag.${RESET}" docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' else - echo "OK, skipped." + read -p "Force a resync now? [y|N] " FORCE_RESYNC + if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then + docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' + else + echo "OK, skipped." + fi fi ;; redis) @@ -586,14 +609,19 @@ function restore() { shift continue else - read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW - if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then - echo "OK, skipped." - shift - continue - else - echo "Stopping mailcow..." + if [[ "${MAILCOW_YES_TO_ALL}" == "1" ]]; then + echo -e "${GREEN_COLOR}Stopping mailcow due to --yes flag.${RESET}" ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down + else + read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW + if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then + echo "OK, skipped." + shift + continue + else + echo "Stopping mailcow..." + ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down + fi fi #docker stop $(docker ps -qf name=mysql-mailcow) if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then From 9d973778099649aa74287d2c6bbc78acb1af9d34 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Tue, 27 Aug 2024 18:36:38 +0300 Subject: [PATCH 29/31] Fix: `find: No such file or directory` when directory not contains data --- helper-scripts/backup_and_restore.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 42aba3e8d7..2b395f0ab9 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -212,6 +212,8 @@ function check_valid_backup_directory() { echo -e "${RED_COLOR}exiting...${RESET}" exit 0 fi + else + echo -e "${GREEN_COLOR}Creating it now due to --yes flag...${RESET}" fi mkdir -p "${BACKUP_LOCATION}" @@ -255,7 +257,7 @@ function check_valid_restore_directory() { } function delete_old_backups() { - if [[ -z $(find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24))) ]]; then + if [[ -z $(find "${MAILCOW_DELETE_LOCATION}"/mailcow-* -maxdepth 0 -mmin +$((${MAILCOW_DELETE_DAYS}*60*24)) 2> /dev/null) ]]; then echo -e "${YELLOW_COLOR}No backups to delete found.${RESET}" exit 0 fi From a20d9276054c323a7c7412ca1ca05f46ba7800d4 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Thu, 29 Aug 2024 15:16:12 +0300 Subject: [PATCH 30/31] Fix dovecot resync: failed: Connection refused Full error message: ``` Error: auth-master: userdb list: connect(/run/dovecot/auth-userdb) failed: Connection refused Panic: file auth-master.c: line 436 (auth_master_unset_io): assertion failed: (conn->to == NULL) ``` --- helper-scripts/backup_and_restore.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 2b395f0ab9..0077d215f7 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -564,6 +564,8 @@ function restore() { # Skip prompt if --yes flag is present if [[ "${MAILCOW_YES_TO_ALL}" == "1" ]]; then echo -e "${GREEN_COLOR}Forcing a resync now due to --yes flag.${RESET}" + # Why sleep 1 second? due to `https://github.com/mailcow/mailcow-dockerized/pull/6030#discussion_r1735971143` + sleep 1s docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' else read -p "Force a resync now? [y|N] " FORCE_RESYNC From e8b6eb34d69c0ef2e9cfe1a286cc5977249a3580 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Thu, 29 Aug 2024 15:22:28 +0300 Subject: [PATCH 31/31] Fix: dovecot resync: failed connection refused --- helper-scripts/backup_and_restore.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 0077d215f7..4ebfbc047f 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -564,8 +564,8 @@ function restore() { # Skip prompt if --yes flag is present if [[ "${MAILCOW_YES_TO_ALL}" == "1" ]]; then echo -e "${GREEN_COLOR}Forcing a resync now due to --yes flag.${RESET}" - # Why sleep 1 second? due to `https://github.com/mailcow/mailcow-dockerized/pull/6030#discussion_r1735971143` - sleep 1s + # Why sleep 2 seconds? due to `https://github.com/mailcow/mailcow-dockerized/pull/6030#discussion_r1735971143` + sleep 2s docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' else read -p "Force a resync now? [y|N] " FORCE_RESYNC