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/dev_tests/test_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

302 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# Test script for backup_and_restore.sh
# Tests backward compatibility with .tar.gz and new .tar.zst format
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BACKUP_IMAGE="${BACKUP_IMAGE:-ghcr.io/mailcow/backup:latest}"
TEST_DIR="/tmp/mailcow_backup_test_$$"
THREADS=2
echo "=== Mailcow Backup & Restore Test Suite ==="
echo "Test directory: ${TEST_DIR}"
echo "Backup image: ${BACKUP_IMAGE}"
echo ""
# Cleanup function
cleanup() {
echo "Cleaning up test files..."
rm -rf "${TEST_DIR}"
docker rmi mailcow-backup-test 2>/dev/null || true
}
trap cleanup EXIT
# Create test directory structure
mkdir -p "${TEST_DIR}"/{test_data,backup_zst,backup_gz,restore_zst,restore_gz,backup_large_zst,backup_large_gz}
echo "Test data for mailcow backup compatibility test" > "${TEST_DIR}/test_data/test.txt"
echo "Additional file to verify complete restore" > "${TEST_DIR}/test_data/test2.txt"
# Build test backup image with zstd support
echo "=== Building backup image with zstd support ==="
docker build -t mailcow-backup-test "${SCRIPT_DIR}/../data/Dockerfiles/backup/" || {
echo "ERROR: Failed to build backup image"
exit 1
}
# Test 1: Create .tar.zst backup
echo ""
echo "=== Test 1: Creating .tar.zst backup ==="
docker run --rm \
-w /data \
-v "${TEST_DIR}/test_data:/data:ro" \
-v "${TEST_DIR}/backup_zst:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}" \
-cvpf /backup/backup_test.tar.zst . \
> /dev/null
echo "✓ .tar.zst backup created: $(ls -lh ${TEST_DIR}/backup_zst/backup_test.tar.zst | awk '{print $5}')"
# Test 2: Create .tar.gz backup
echo ""
echo "=== Test 2: Creating .tar.gz backup (legacy) ==="
docker run --rm \
-w /data \
-v "${TEST_DIR}/test_data:/data:ro" \
-v "${TEST_DIR}/backup_gz:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="pigz --rsyncable -p ${THREADS}" \
-cvpf /backup/backup_test.tar.gz . \
> /dev/null
echo "✓ .tar.gz backup created: $(ls -lh ${TEST_DIR}/backup_gz/backup_test.tar.gz | awk '{print $5}')"
# Test 3: Test get_archive_info function
echo ""
echo "=== Test 3: Testing get_archive_info function ==="
# Extract and test the function directly
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
}
# Test with .tar.zst
result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_zst")
if [[ "${result}" =~ "zstd" ]]; then
echo "✓ Correctly detects .tar.zst and returns zstd decompressor"
else
echo "✗ Failed to detect .tar.zst"
exit 1
fi
# Test with .tar.gz
result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_gz")
if [[ "${result}" =~ "pigz" ]]; then
echo "✓ Correctly detects .tar.gz and returns pigz decompressor"
else
echo "✗ Failed to detect .tar.gz"
exit 1
fi
# Test with no file
result=$(get_archive_info "backup_test" "${TEST_DIR}")
if [[ -z "${result}" ]]; then
echo "✓ Correctly returns empty when no backup file found"
else
echo "✗ Should return empty but got: ${result}"
exit 1
fi
# Test 4: Restore from .tar.zst
echo ""
echo "=== Test 4: Restoring from .tar.zst ==="
docker run --rm \
-w /restore \
-v "${TEST_DIR}/backup_zst:/backup:ro" \
-v "${TEST_DIR}/restore_zst:/restore" \
mailcow-backup-test \
/bin/tar --use-compress-program="zstd -d -T${THREADS}" -xvpf /backup/backup_test.tar.zst \
> /dev/null 2>&1
if [[ -f "${TEST_DIR}/restore_zst/test.txt" ]] && \
[[ -f "${TEST_DIR}/restore_zst/test2.txt" ]]; then
echo "✓ Successfully restored from .tar.zst"
else
echo "✗ Failed to restore from .tar.zst"
ls -la "${TEST_DIR}/restore_zst/" || true
exit 1
fi
# Test 5: Restore from .tar.gz
echo ""
echo "=== Test 5: Restoring from .tar.gz (backward compatibility) ==="
docker run --rm \
-w /restore \
-v "${TEST_DIR}/backup_gz:/backup:ro" \
-v "${TEST_DIR}/restore_gz:/restore" \
mailcow-backup-test \
/bin/tar --use-compress-program="pigz -d -p ${THREADS}" -xvpf /backup/backup_test.tar.gz \
> /dev/null 2>&1
if [[ -f "${TEST_DIR}/restore_gz/test.txt" ]] && \
[[ -f "${TEST_DIR}/restore_gz/test2.txt" ]]; then
echo "✓ Successfully restored from .tar.gz (backward compatible)"
else
echo "✗ Failed to restore from .tar.gz"
ls -la "${TEST_DIR}/restore_gz/" || true
exit 1
fi
# Test 6: Verify content integrity
echo ""
echo "=== Test 6: Verifying content integrity ==="
original_content=$(cat "${TEST_DIR}/test_data/test.txt")
zst_content=$(cat "${TEST_DIR}/restore_zst/test.txt")
gz_content=$(cat "${TEST_DIR}/restore_gz/test.txt")
if [[ "${original_content}" == "${zst_content}" ]] && \
[[ "${original_content}" == "${gz_content}" ]]; then
echo "✓ Content integrity verified for both formats"
else
echo "✗ Content mismatch detected"
exit 1
fi
# Test 7: Compare compression ratios
echo ""
echo "=== Test 7: Compression comparison ==="
zst_size=$(stat -f%z "${TEST_DIR}/backup_zst/backup_test.tar.zst" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_zst/backup_test.tar.zst")
gz_size=$(stat -f%z "${TEST_DIR}/backup_gz/backup_test.tar.gz" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_gz/backup_test.tar.gz")
improvement=$(echo "scale=2; (${gz_size} - ${zst_size}) * 100 / ${gz_size}" | bc)
echo " Small files - .tar.gz size: ${gz_size} bytes"
echo " Small files - .tar.zst size: ${zst_size} bytes"
echo " Small files - Improvement: ${improvement}% smaller with zstd"
# Test 8: Error handling - missing backup file
echo ""
echo "=== Test 8: Error handling - Missing backup file ==="
result=$(get_archive_info "nonexistent_backup" "${TEST_DIR}/backup_zst")
if [[ -z "${result}" ]]; then
echo "✓ Correctly handles missing backup files"
else
echo "✗ Should return empty for missing files"
exit 1
fi
# Test 9: Error handling - Empty directory
echo ""
echo "=== Test 9: Error handling - Empty directory ==="
mkdir -p "${TEST_DIR}/empty_dir"
result=$(get_archive_info "backup_test" "${TEST_DIR}/empty_dir")
if [[ -z "${result}" ]]; then
echo "✓ Correctly handles empty directories"
else
echo "✗ Should return empty for empty directories"
exit 1
fi
# Test 10: Priority test - .tar.zst preferred over .tar.gz
echo ""
echo "=== Test 10: Format priority - .tar.zst preferred ==="
mkdir -p "${TEST_DIR}/both_formats"
touch "${TEST_DIR}/both_formats/backup_test.tar.gz"
touch "${TEST_DIR}/both_formats/backup_test.tar.zst"
result=$(get_archive_info "backup_test" "${TEST_DIR}/both_formats")
if [[ "${result}" =~ "zstd" ]]; then
echo "✓ Correctly prefers .tar.zst when both formats exist"
else
echo "✗ Should prefer .tar.zst over .tar.gz"
exit 1
fi
# Test 11: Large file compression test
echo ""
echo "=== Test 11: Large file compression test ==="
mkdir -p "${TEST_DIR}/large_data"
# Create ~10MB of compressible data (log-like content)
for i in {1..50000}; do
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: Processing email message $i from user@example.com to recipient@domain.com" >> "${TEST_DIR}/large_data/maillog.txt"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: SMTP connection established from 192.168.1.$((i % 255))" >> "${TEST_DIR}/large_data/maillog.txt"
done 2>/dev/null
# Get size (portable: works on Linux and macOS)
if du --version 2>/dev/null | grep -q GNU; then
original_size=$(du -sb "${TEST_DIR}/large_data" | cut -f1)
else
# macOS
original_size=$(find "${TEST_DIR}/large_data" -type f -exec stat -f%z {} \; | awk '{sum+=$1} END {print sum}')
fi
echo " Original data size: $(echo "scale=2; ${original_size} / 1024 / 1024" | bc) MB"
# Backup with zstd
docker run --rm \
-w /data \
-v "${TEST_DIR}/large_data:/data:ro" \
-v "${TEST_DIR}/backup_large_zst:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}" \
-cvpf /backup/backup_large.tar.zst . \
> /dev/null 2>&1
# Backup with pigz
docker run --rm \
-w /data \
-v "${TEST_DIR}/large_data:/data:ro" \
-v "${TEST_DIR}/backup_large_gz:/backup" \
mailcow-backup-test \
/bin/tar --use-compress-program="pigz --rsyncable -p ${THREADS}" \
-cvpf /backup/backup_large.tar.gz . \
> /dev/null 2>&1
zst_large_size=$(stat -f%z "${TEST_DIR}/backup_large_zst/backup_large.tar.zst" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_large_zst/backup_large.tar.zst" 2>/dev/null || echo "0")
gz_large_size=$(stat -f%z "${TEST_DIR}/backup_large_gz/backup_large.tar.gz" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_large_gz/backup_large.tar.gz" 2>/dev/null || echo "0")
if [[ ${zst_large_size} -gt 0 ]] && [[ ${gz_large_size} -gt 0 ]]; then
large_improvement=$(echo "scale=2; (${gz_large_size} - ${zst_large_size}) * 100 / ${gz_large_size}" | bc)
echo " .tar.gz compressed: $(echo "scale=2; ${gz_large_size} / 1024 / 1024" | bc) MB"
echo " .tar.zst compressed: $(echo "scale=2; ${zst_large_size} / 1024 / 1024" | bc) MB"
echo " Improvement: ${large_improvement}% smaller with zstd"
else
echo " ✗ Failed to get file sizes"
exit 1
fi
if [[ $(echo "${large_improvement} > 0" | bc) -eq 1 ]]; then
echo "✓ zstd provides better compression on realistic data"
else
echo "⚠ zstd compression similar or worse than gzip (unusual but not critical)"
fi
# Test 12: Thread scaling test
echo ""
echo "=== Test 12: Multi-threading verification ==="
# This test verifies that different thread counts work (not measuring speed difference)
for thread_count in 1 4; do
THREADS=${thread_count}
result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_zst")
if [[ "${result}" =~ "-T${thread_count}" ]]; then
echo "✓ Thread count ${thread_count} correctly configured"
else
echo "✗ Thread count not properly applied"
exit 1
fi
done
echo ""
echo "=== All tests passed! ==="
echo ""
echo "Summary:"
echo " ✓ zstd compression working"
echo " ✓ pigz compression working (legacy)"
echo " ✓ zstd decompression working"
echo " ✓ pigz decompression working (backward compatible)"
echo " ✓ Archive detection working"
echo " ✓ Content integrity verified"
echo " ✓ Format priority correct (.tar.zst preferred)"
echo " ✓ Error handling for missing files"
echo " ✓ Error handling for empty directories"
echo " ✓ Multi-threading configuration verified"
echo " ✓ Large file compression: ${large_improvement}% improvement"
echo " ✓ Small file compression: ${improvement}% improvement"