mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2025-12-12 17:36:01 +00:00
* 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>
302 lines
10 KiB
Bash
Executable File
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"
|