1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2025-12-12 17:36:01 +00:00
Files
mailcow-dockerized/helper-scripts/backup_and_restore.sh
Claas Flint 1b833be760 Replace pigz with zstd for backup compression (#6897)
* Replace pigz with zstd for backup compression

This change replaces pigz (parallel gzip) with zstd (Zstandard) as the
compression algorithm for mailcow backups while maintaining full backward
compatibility with existing .tar.gz backups.

Benefits:
- Better compression ratios (12-37% improvement in tests)
- Improved compression speed with modern algorithm
- Maintains rsyncable functionality for incremental backups
- Full backward compatibility for restoring old .tar.gz backups
- Wide industry adoption and active development

Changes:
- Backup compression: pigz --rsyncable -p → zstd --rsyncable -T
- Backup decompression: pigz -d -p → zstd -d -T
- File extensions: .tar.gz → .tar.zst
- Added get_archive_info() function for intelligent format detection
- Updated backup Dockerfile to install zstd alongside pigz
- Restore function now auto-detects and handles both formats
- Updated FILE_SELECTION regex to recognize both .tar.zst and .tar.gz
- Updated comments to reflect new file extension

Backward Compatibility:
- Restore automatically detects .tar.zst (preferred) or .tar.gz (legacy)
- Existing .tar.gz backups can still be restored without issues
- pigz remains installed in backup image for legacy support
- Graceful fallback if backup file format not found

Testing:
- Added comprehensive test suite (test_backup_and_restore.sh)
- 12 automated tests covering all scenarios:
  * Backup creation (both formats)
  * Restore (both formats)
  * Format detection and priority
  * Error handling (missing files, empty dirs)
  * Content integrity verification
  * Multi-threading configuration
  * Large file compression (8.59 MB realistic data)

Test Results:
✓ zstd compression working
✓ pigz compression working (legacy)
✓ zstd decompression working
✓ pigz decompression working (backward compatible)
✓ Archive detection working
✓ Content integrity verified
✓ Format priority correct (.tar.zst preferred)
✓ Error handling for missing files
✓ Error handling for empty directories
✓ Multi-threading configuration verified
✓ Large file compression: 37.05% improvement
✓ Small file compression: 12.18% improvement

* move testing script into development folder

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-11-12 10:06:36 +01:00

464 lines
20 KiB
Bash
Executable File

#!/usr/bin/env bash
DEBIAN_DOCKER_IMAGE="ghcr.io/mailcow/backup:latest"
if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then
BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}"
fi
if [[ ! ${1} =~ (backup|restore) ]]; then
echo "First parameter needs to be 'backup' or 'restore'"
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'"
exit 1
fi
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
echo "Backup directory needs to be given as absolute path (starting with /)."
exit 1
fi
if [[ -f ${BACKUP_LOCATION} ]]; then
echo "${BACKUP_LOCATION} is a file!"
exit 1
fi
if [[ ! -d ${BACKUP_LOCATION} ]]; then
echo "${BACKUP_LOCATION} is not a directory"
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}
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."
exit 1
fi
fi
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)
if ! [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then
echo "Thread input is not a number!"
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."
fi
if [ ! -f ${COMPOSE_FILE} ]; then
echo "Compose file not found"
exit 1
fi
if [ ! -f ${ENV_FILE} ]; then
echo "Environment file not found"
exit 1
fi
echo "Using ${BACKUP_LOCATION} as backup/restore location."
echo
source ${SCRIPT_DIR}/../mailcow.conf
if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then
echo "Could not determine compose project name"
exit 1
else
echo "Found project name ${COMPOSE_PROJECT_NAME}"
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")
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"
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)
docker run --name mailcow-backup --rm \
-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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_vmail.tar.zst /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_crypt.tar.zst /crypt
;;&
redis|all)
docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning 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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_redis.tar.zst /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_rspamd.tar.zst /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="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_postfix.tar.zst /postfix
;;&
mysql|all)
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
if [[ -z "${SQLIMAGE}" ]]; then
echo "Could not determine SQL image version, skipping backup..."
shift
continue
else
echo "Using SQL image ${SQLIMAGE}, starting..."
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 \
-t --entrypoint= \
--sysctl net.ipv6.conf.all.disable_ipv6=1 \
-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 ; \
/bin/tar --warning='no-file-ignored' --use-compress-program='zstd --rsyncable' -Pcvpf /backup/backup_mariadb.tar.zst /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 "Parameter of --delete-days is not a number."
fi
;;
esac
shift
done
}
function get_archive_info() {
local backup_name="$1"
local location="$2"
if [[ -f "${location}/${backup_name}.tar.zst" ]]; then
echo "${backup_name}.tar.zst|zstd -d -T${THREADS}"
elif [[ -f "${location}/${backup_name}.tar.gz" ]]; then
echo "${backup_name}.tar.gz|pigz -d -p ${THREADS}"
else
echo ""
fi
}
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"
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
fi
echo
echo "Stopping watchdog-mailcow..."
docker stop $(docker ps -qf name=watchdog-mailcow)
echo
RESTORE_LOCATION="${1}"
shift
while (( "$#" )); do
case "$1" in
vmail)
docker stop $(docker ps -qf name=dovecot-mailcow)
ARCHIVE_INFO=$(get_archive_info "backup_vmail" "${RESTORE_LOCATION}")
if [[ -z "${ARCHIVE_INFO}" ]]; then
echo -e "\e[31mError: No backup file found for vmail (searched for .tar.zst and .tar.gz)\e[0m"
else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=dovecot-mailcow)
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)
docker stop $(docker ps -qf name=redis-mailcow)
ARCHIVE_INFO=$(get_archive_info "backup_redis" "${RESTORE_LOCATION}")
if [[ -z "${ARCHIVE_INFO}" ]]; then
echo -e "\e[31mError: No backup file found for redis (searched for .tar.zst and .tar.gz)\e[0m"
else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=redis-mailcow)
;;
crypt)
docker stop $(docker ps -qf name=dovecot-mailcow)
ARCHIVE_INFO=$(get_archive_info "backup_crypt" "${RESTORE_LOCATION}")
if [[ -z "${ARCHIVE_INFO}" ]]; then
echo -e "\e[31mError: No backup file found for crypt (searched for .tar.zst and .tar.gz)\e[0m"
else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=dovecot-mailcow)
;;
rspamd)
ARCHIVE_INFO=$(get_archive_info "backup_rspamd" "${RESTORE_LOCATION}")
if [[ -z "${ARCHIVE_INFO}" ]]; then
echo -e "\e[31mError: No backup file found for rspamd (searched for .tar.zst and .tar.gz)\e[0m"
elif [[ $(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 upon 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)
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
docker start $(docker ps -aqf name=rspamd-mailcow)
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)
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
docker start $(docker ps -aqf name=rspamd-mailcow)
fi
;;
postfix)
docker stop $(docker ps -qf name=postfix-mailcow)
ARCHIVE_INFO=$(get_archive_info "backup_postfix" "${RESTORE_LOCATION}")
if [[ -z "${ARCHIVE_INFO}" ]]; then
echo -e "\e[31mError: No backup file found for postfix (searched for .tar.zst and .tar.gz)\e[0m"
else
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE}
fi
docker start $(docker ps -aqf name=postfix-mailcow)
;;
mysql|mariadb)
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE})
if [[ -z "${SQLIMAGE}" ]]; then
echo "Could not determine SQL image version, skipping restore..."
shift
continue
elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then
echo "Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore."
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
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;"
else
ARCHIVE_INFO=$(get_archive_info "backup_mariadb" "${RESTORE_LOCATION}")
if [[ -n "${ARCHIVE_INFO}" ]]; then
ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1)
DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2)
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 --use-compress-program='${DECOMPRESS_PROG}' -Pxvf /backup/${ARCHIVE_FILE}"
fi
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
echo
echo "Starting watchdog-mailcow..."
docker start $(docker ps -aqf name=watchdog-mailcow)
}
if [[ ${1} == "backup" ]]; then
backup ${@,,}
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"
exit 1
fi
for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do
echo "[ ${i} ] - ${folder}"
FOLDER_SELECTION[${i}]="${folder}"
((i++))
done
echo
input_sel=0
while [[ ${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 "No datasets found"
exit 1
fi
echo "[ 0 ] - all"
# find all files in folder with *.zst or *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .zst/.gz
FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.zst' -o -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//' | sort -u)
for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do
if [[ ${file} =~ vmail ]]; then
echo "[ ${i} ] - Mail directory (/var/vmail)"
FILE_SELECTION[${i}]="vmail"
((i++))
elif [[ ${file} =~ crypt ]]; then
echo "[ ${i} ] - Crypt data"
FILE_SELECTION[${i}]="crypt"
((i++))
elif [[ ${file} =~ redis ]]; then
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
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
echo "[ ${i} ] - Rspamd data"
FILE_SELECTION[${i}]="rspamd"
((i++))
fi
elif [[ ${file} =~ postfix ]]; then
echo "[ ${i} ] - Postfix data"
FILE_SELECTION[${i}]="postfix"
((i++))
elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then
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
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