✨ New Features: - Complete backup lifecycle management (create, list, download, delete, cleanup) - Web-based backup interface with real-time status updates - Individual backup deletion and bulk cleanup for old backups - Docker-aware backup operations with volume persistence - Automated backup scheduling and retention policies 📁 Added Files: - backup.py - Core backup script for creating timestamped archives - docker_backup.sh - Docker-compatible backup wrapper script - app/templates/backup.html - Web interface for backup management - BACKUP_SYSTEM.md - Comprehensive backup system documentation - BACKUP_GUIDE.md - Quick reference guide for backup operations 🔧 Enhanced Files: - Dockerfile - Added backup.py copy for container availability - docker-compose.yml - Added backup volume mount for persistence - app/routes/api.py - Added backup API endpoints (create, list, delete, cleanup) - app/routes/main.py - Added backup management route - app/templates/index.html - Added backup management navigation - README.md - Updated with backup system overview and quick start 🎯 Key Improvements: - Fixed backup creation errors in Docker environment - Added Docker-aware path detection for container operations - Implemented proper error handling and user confirmation dialogs - Added real-time backup status updates via JavaScript - Enhanced data persistence with volume mounting 💡 Use Cases: - Data protection and disaster recovery - Environment migration and cloning - Development data management - Automated maintenance workflows
408 lines
12 KiB
Bash
Executable File
408 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
||
"""
|
||
QR Code Manager - Docker Backup Script
|
||
|
||
This script provides backup and restore functionality for dockerized QR Code Manager.
|
||
It handles both data volumes and complete application backups.
|
||
|
||
Usage:
|
||
./docker_backup.sh [command] [options]
|
||
|
||
Commands:
|
||
backup-data - Backup data volumes only
|
||
backup-full - Full backup including application and data
|
||
restore - Restore from backup
|
||
list - List available backups
|
||
cleanup - Remove old backups
|
||
schedule - Set up automated backups
|
||
|
||
Examples:
|
||
./docker_backup.sh backup-data
|
||
./docker_backup.sh backup-full
|
||
./docker_backup.sh restore backup_20240801_120000.tar.gz
|
||
./docker_backup.sh list
|
||
"""
|
||
|
||
set -e # Exit on any error
|
||
|
||
# Configuration
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
BACKUP_DIR="${SCRIPT_DIR}/backups"
|
||
CONTAINER_NAME="qr-code-manager"
|
||
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml"
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Helper functions
|
||
log_info() {
|
||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}✅ $1${NC}"
|
||
}
|
||
|
||
log_warning() {
|
||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}❌ $1${NC}"
|
||
}
|
||
|
||
# Ensure backup directory exists
|
||
mkdir -p "${BACKUP_DIR}"
|
||
|
||
# Check if Docker and docker-compose are available
|
||
check_dependencies() {
|
||
if ! command -v docker &> /dev/null; then
|
||
log_error "Docker is not installed or not in PATH"
|
||
exit 1
|
||
fi
|
||
|
||
if command -v docker &> /dev/null && docker compose version &> /dev/null 2>&1; then
|
||
# Use docker compose (newer syntax)
|
||
COMPOSE_CMD="docker compose"
|
||
elif command -v docker-compose &> /dev/null; then
|
||
# Fallback to docker-compose (older syntax)
|
||
COMPOSE_CMD="docker-compose"
|
||
else
|
||
log_error "Neither 'docker compose' nor 'docker-compose' is available"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Get timestamp for backup naming
|
||
get_timestamp() {
|
||
date +"%Y%m%d_%H%M%S"
|
||
}
|
||
|
||
# Check if container is running
|
||
is_container_running() {
|
||
docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"
|
||
}
|
||
|
||
# Backup data volumes only
|
||
backup_data() {
|
||
local timestamp=$(get_timestamp)
|
||
local backup_name="qr_data_backup_${timestamp}.tar.gz"
|
||
local backup_path="${BACKUP_DIR}/${backup_name}"
|
||
|
||
log_info "Creating data backup: ${backup_name}"
|
||
|
||
if is_container_running; then
|
||
log_info "Container is running, creating hot backup..."
|
||
|
||
# Create backup from running container
|
||
docker exec "${CONTAINER_NAME}" tar czf /tmp/data_backup.tar.gz \
|
||
-C /app data/ || {
|
||
log_error "Failed to create backup inside container"
|
||
return 1
|
||
}
|
||
|
||
# Copy backup from container to host
|
||
docker cp "${CONTAINER_NAME}:/tmp/data_backup.tar.gz" "${backup_path}" || {
|
||
log_error "Failed to copy backup from container"
|
||
return 1
|
||
}
|
||
|
||
# Cleanup temp file in container
|
||
docker exec "${CONTAINER_NAME}" rm -f /tmp/data_backup.tar.gz
|
||
|
||
else
|
||
log_info "Container is not running, creating backup from volumes..."
|
||
|
||
# Create backup using temporary container
|
||
docker run --rm \
|
||
-v "${SCRIPT_DIR}/data:/source/data:ro" \
|
||
-v "${BACKUP_DIR}:/backup" \
|
||
alpine:latest tar czf "/backup/${backup_name}" -C /source data/ || {
|
||
log_error "Failed to create backup from volumes"
|
||
return 1
|
||
}
|
||
fi
|
||
|
||
# Add metadata
|
||
local metadata_file="${BACKUP_DIR}/${backup_name%.tar.gz}.json"
|
||
cat > "${metadata_file}" << EOF
|
||
{
|
||
"backup_type": "data",
|
||
"timestamp": "$(date -Iseconds)",
|
||
"container_name": "${CONTAINER_NAME}",
|
||
"backup_method": "$(if is_container_running; then echo 'hot'; else echo 'cold'; fi)",
|
||
"backup_size": "$(stat -f%z "${backup_path}" 2>/dev/null || stat -c%s "${backup_path}" 2>/dev/null || echo 'unknown')"
|
||
}
|
||
EOF
|
||
|
||
log_success "Data backup created: ${backup_path}"
|
||
log_info "Backup size: $(du -h "${backup_path}" | cut -f1)"
|
||
}
|
||
|
||
# Full backup including application
|
||
backup_full() {
|
||
local timestamp=$(get_timestamp)
|
||
local backup_name="qr_full_backup_${timestamp}.tar.gz"
|
||
local backup_path="${BACKUP_DIR}/${backup_name}"
|
||
|
||
log_info "Creating full backup: ${backup_name}"
|
||
|
||
# Stop container for consistent backup
|
||
local was_running=false
|
||
if is_container_running; then
|
||
log_warning "Stopping container for consistent backup..."
|
||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" down
|
||
was_running=true
|
||
fi
|
||
|
||
# Create full backup
|
||
tar czf "${backup_path}" \
|
||
--exclude="backups" \
|
||
--exclude=".git" \
|
||
--exclude="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
--exclude="*.log" \
|
||
-C "${SCRIPT_DIR}/.." \
|
||
"$(basename "${SCRIPT_DIR}")" || {
|
||
log_error "Failed to create full backup"
|
||
return 1
|
||
}
|
||
|
||
# Restart container if it was running
|
||
if [ "$was_running" = true ]; then
|
||
log_info "Restarting container..."
|
||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d
|
||
fi
|
||
|
||
# Add metadata
|
||
local metadata_file="${BACKUP_DIR}/${backup_name%.tar.gz}.json"
|
||
cat > "${metadata_file}" << EOF
|
||
{
|
||
"backup_type": "full",
|
||
"timestamp": "$(date -Iseconds)",
|
||
"container_name": "${CONTAINER_NAME}",
|
||
"backup_method": "cold",
|
||
"backup_size": "$(stat -f%z "${backup_path}" 2>/dev/null || stat -c%s "${backup_path}" 2>/dev/null || echo 'unknown')"
|
||
}
|
||
EOF
|
||
|
||
log_success "Full backup created: ${backup_path}"
|
||
log_info "Backup size: $(du -h "${backup_path}" | cut -f1)"
|
||
}
|
||
|
||
# List available backups
|
||
list_backups() {
|
||
log_info "Available backups:"
|
||
echo "$(printf '%s' '─%.0s' {1..80})"
|
||
|
||
if [ ! "$(ls -A "${BACKUP_DIR}"/*.tar.gz 2>/dev/null)" ]; then
|
||
echo "No backups found"
|
||
return
|
||
fi
|
||
|
||
for backup_file in "${BACKUP_DIR}"/*.tar.gz; do
|
||
if [ -f "$backup_file" ]; then
|
||
local basename=$(basename "$backup_file")
|
||
local size=$(du -h "$backup_file" | cut -f1)
|
||
local date=$(stat -f%Sm -t "%Y-%m-%d %H:%M" "$backup_file" 2>/dev/null || \
|
||
stat -c%y "$backup_file" 2>/dev/null | cut -d' ' -f1-2)
|
||
|
||
# Determine backup type
|
||
local type="Unknown"
|
||
if [[ "$basename" == *"data"* ]]; then
|
||
type="📄 Data"
|
||
elif [[ "$basename" == *"full"* ]]; then
|
||
type="📦 Full"
|
||
fi
|
||
|
||
printf " %-12s | %-40s | %8s | %s\n" "$type" "$basename" "$size" "$date"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# Restore from backup
|
||
restore_backup() {
|
||
local backup_file="$1"
|
||
|
||
if [ -z "$backup_file" ]; then
|
||
log_error "Please specify backup file to restore"
|
||
echo "Available backups:"
|
||
list_backups
|
||
return 1
|
||
fi
|
||
|
||
# Check if backup file exists
|
||
local backup_path="${backup_file}"
|
||
if [ ! -f "$backup_path" ]; then
|
||
backup_path="${BACKUP_DIR}/${backup_file}"
|
||
if [ ! -f "$backup_path" ]; then
|
||
log_error "Backup file not found: $backup_file"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
log_warning "Restoring from backup: $(basename "$backup_path")"
|
||
|
||
# Confirm restoration
|
||
echo -n "This will overwrite current data. Continue? (y/N): "
|
||
read -r confirm
|
||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||
log_info "Restoration cancelled"
|
||
return 0
|
||
fi
|
||
|
||
# Stop container
|
||
if is_container_running; then
|
||
log_info "Stopping container..."
|
||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" down
|
||
fi
|
||
|
||
# Determine backup type and restore accordingly
|
||
if [[ "$(basename "$backup_path")" == *"data"* ]]; then
|
||
log_info "Restoring data backup..."
|
||
|
||
# Backup current data
|
||
if [ -d "${SCRIPT_DIR}/data" ]; then
|
||
mv "${SCRIPT_DIR}/data" "${SCRIPT_DIR}/data.backup.$(get_timestamp)"
|
||
fi
|
||
|
||
# Extract data backup
|
||
tar xzf "$backup_path" -C "${SCRIPT_DIR}" || {
|
||
log_error "Failed to extract data backup"
|
||
return 1
|
||
}
|
||
|
||
elif [[ "$(basename "$backup_path")" == *"full"* ]]; then
|
||
log_info "Restoring full backup..."
|
||
|
||
# Create restoration directory
|
||
local restore_dir="${SCRIPT_DIR}/../qr-restore-$(get_timestamp)"
|
||
mkdir -p "$restore_dir"
|
||
|
||
# Extract full backup
|
||
tar xzf "$backup_path" -C "$restore_dir" || {
|
||
log_error "Failed to extract full backup"
|
||
return 1
|
||
}
|
||
|
||
log_warning "Full backup extracted to: $restore_dir"
|
||
log_warning "Please manually review and copy files as needed"
|
||
return 0
|
||
fi
|
||
|
||
# Restart container
|
||
log_info "Starting container..."
|
||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d
|
||
|
||
log_success "Restoration completed successfully"
|
||
}
|
||
|
||
# Cleanup old backups
|
||
cleanup_backups() {
|
||
local keep_count=${1:-10}
|
||
|
||
log_info "Cleaning up old backups (keeping $keep_count most recent)"
|
||
|
||
# Get list of backup files sorted by modification time (newest first)
|
||
local backup_files=($(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null || true))
|
||
|
||
if [ ${#backup_files[@]} -le $keep_count ]; then
|
||
log_info "No cleanup needed (${#backup_files[@]} backups found)"
|
||
return
|
||
fi
|
||
|
||
# Remove old backups
|
||
for ((i=$keep_count; i<${#backup_files[@]}; i++)); do
|
||
local backup_file="${backup_files[$i]}"
|
||
local metadata_file="${backup_file%.tar.gz}.json"
|
||
|
||
log_info "Removing old backup: $(basename "$backup_file")"
|
||
rm -f "$backup_file" "$metadata_file"
|
||
done
|
||
|
||
log_success "Cleanup completed"
|
||
}
|
||
|
||
# Set up automated backups
|
||
schedule_backups() {
|
||
log_info "Setting up automated backups with cron..."
|
||
|
||
# Create backup script for cron
|
||
local cron_script="${SCRIPT_DIR}/automated_backup.sh"
|
||
cat > "$cron_script" << 'EOF'
|
||
#!/bin/bash
|
||
# Automated backup script for QR Code Manager
|
||
cd "$(dirname "$0")"
|
||
./docker_backup.sh backup-data
|
||
if [ $(date +%u) -eq 1 ]; then # Monday
|
||
./docker_backup.sh backup-full
|
||
fi
|
||
./docker_backup.sh cleanup 15
|
||
EOF
|
||
chmod +x "$cron_script"
|
||
|
||
# Suggest cron entries
|
||
echo
|
||
log_info "Automated backup script created: $cron_script"
|
||
echo
|
||
echo "Add this to your crontab (crontab -e) for daily backups:"
|
||
echo "# QR Code Manager daily backup at 2 AM"
|
||
echo "0 2 * * * $cron_script >> ${SCRIPT_DIR}/backup.log 2>&1"
|
||
echo
|
||
echo "Or for hourly data backups:"
|
||
echo "# QR Code Manager hourly data backup"
|
||
echo "0 * * * * ${SCRIPT_DIR}/docker_backup.sh backup-data >> ${SCRIPT_DIR}/backup.log 2>&1"
|
||
}
|
||
|
||
# Main script logic
|
||
main() {
|
||
check_dependencies
|
||
|
||
case "${1:-}" in
|
||
"backup-data")
|
||
backup_data
|
||
;;
|
||
"backup-full")
|
||
backup_full
|
||
;;
|
||
"restore")
|
||
restore_backup "$2"
|
||
;;
|
||
"list")
|
||
list_backups
|
||
;;
|
||
"cleanup")
|
||
cleanup_backups "$2"
|
||
;;
|
||
"schedule")
|
||
schedule_backups
|
||
;;
|
||
*)
|
||
echo "QR Code Manager - Docker Backup Script"
|
||
echo
|
||
echo "Usage: $0 [command] [options]"
|
||
echo
|
||
echo "Commands:"
|
||
echo " backup-data - Backup data volumes only"
|
||
echo " backup-full - Full backup including application and data"
|
||
echo " restore <file> - Restore from backup"
|
||
echo " list - List available backups"
|
||
echo " cleanup [n] - Remove old backups (keep n most recent, default: 10)"
|
||
echo " schedule - Set up automated backups"
|
||
echo
|
||
echo "Examples:"
|
||
echo " $0 backup-data"
|
||
echo " $0 backup-full"
|
||
echo " $0 restore qr_data_backup_20240801_120000.tar.gz"
|
||
echo " $0 list"
|
||
echo " $0 cleanup 5"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|