Files
qr-code_manager/backup.py
ske087 9e4c21996b 🔄 Add Comprehensive Backup Management System
 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
2025-08-01 13:01:15 -04:00

378 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
"""
QR Code Manager - Backup Utility
This script creates comprehensive backups of the QR Code Manager application
including data files, configuration, and optionally the entire application.
Usage:
python backup.py [options]
Options:
--data-only : Backup only data files (default)
--full : Full backup including application files
--restore : Restore from backup
--list : List available backups
--config : Backup configuration files only
--auto : Automated backup (for cron jobs)
"""
import os
import sys
import json
import shutil
import tarfile
import argparse
from datetime import datetime
from pathlib import Path
class QRCodeManagerBackup:
def __init__(self):
self.app_root = Path(__file__).parent
self.backup_dir = self.app_root / "backups"
self.data_dir = self.app_root / "data"
self.config_files = [
".env",
".env.production",
"docker-compose.yml",
"Dockerfile",
"gunicorn.conf.py",
"requirements.txt"
]
self.data_files = [
"data/link_pages.json",
"data/qr_codes.json",
"data/short_urls.json"
]
# Ensure backup directory exists
self.backup_dir.mkdir(exist_ok=True)
def get_timestamp(self):
"""Get current timestamp for backup naming"""
return datetime.now().strftime("%Y%m%d_%H%M%S")
def create_data_backup(self, backup_name=None):
"""Create backup of data files only"""
if not backup_name:
backup_name = f"qr_data_backup_{self.get_timestamp()}.tar.gz"
backup_path = self.backup_dir / backup_name
print(f"🗂️ Creating data backup: {backup_name}")
with tarfile.open(backup_path, "w:gz") as tar:
# Add data files
for data_file in self.data_files:
file_path = self.app_root / data_file
if file_path.exists():
tar.add(file_path, arcname=data_file)
print(f" ✅ Added: {data_file}")
else:
print(f" ⚠️ Missing: {data_file}")
# Add backup metadata
metadata = {
"backup_type": "data",
"timestamp": datetime.now().isoformat(),
"app_version": self.get_app_version(),
"files_included": self.data_files
}
metadata_path = self.app_root / "backup_metadata.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
tar.add(metadata_path, arcname="backup_metadata.json")
metadata_path.unlink() # Remove temp file
print(f"✅ Data backup created: {backup_path}")
return backup_path
def create_config_backup(self, backup_name=None):
"""Create backup of configuration files only"""
if not backup_name:
backup_name = f"qr_config_backup_{self.get_timestamp()}.tar.gz"
backup_path = self.backup_dir / backup_name
print(f"⚙️ Creating config backup: {backup_name}")
with tarfile.open(backup_path, "w:gz") as tar:
# Add config files
for config_file in self.config_files:
file_path = self.app_root / config_file
if file_path.exists():
tar.add(file_path, arcname=config_file)
print(f" ✅ Added: {config_file}")
else:
print(f" ⚠️ Missing: {config_file}")
# Add backup metadata
metadata = {
"backup_type": "config",
"timestamp": datetime.now().isoformat(),
"app_version": self.get_app_version(),
"files_included": self.config_files
}
metadata_path = self.app_root / "backup_metadata.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
tar.add(metadata_path, arcname="backup_metadata.json")
metadata_path.unlink() # Remove temp file
print(f"✅ Config backup created: {backup_path}")
return backup_path
def create_full_backup(self, backup_name=None):
"""Create full backup including application files"""
if not backup_name:
backup_name = f"qr_full_backup_{self.get_timestamp()}.tar.gz"
backup_path = self.backup_dir / backup_name
print(f"📦 Creating full backup: {backup_name}")
# Exclude patterns
exclude_patterns = [
"__pycache__",
"*.pyc",
".git",
"backups",
"*.log",
".dockerignore",
"backup.py"
]
def exclude_filter(tarinfo):
for pattern in exclude_patterns:
if pattern in tarinfo.name:
return None
return tarinfo
with tarfile.open(backup_path, "w:gz") as tar:
# Add entire application directory
tar.add(self.app_root, arcname="qr-code-manager", filter=exclude_filter)
# Add backup metadata
metadata = {
"backup_type": "full",
"timestamp": datetime.now().isoformat(),
"app_version": self.get_app_version(),
"excluded_patterns": exclude_patterns
}
metadata_path = self.app_root / "backup_metadata.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
tar.add(metadata_path, arcname="backup_metadata.json")
metadata_path.unlink() # Remove temp file
print(f"✅ Full backup created: {backup_path}")
return backup_path
def list_backups(self):
"""List all available backups"""
print("📋 Available backups:")
print("-" * 60)
backup_files = sorted(self.backup_dir.glob("*.tar.gz"), reverse=True)
if not backup_files:
print(" No backups found")
return
for backup_file in backup_files:
# Get file info
stat = backup_file.stat()
size_mb = stat.st_size / (1024 * 1024)
modified = datetime.fromtimestamp(stat.st_mtime)
# Try to get backup type from filename
if "data" in backup_file.name:
backup_type = "📄 Data"
elif "config" in backup_file.name:
backup_type = "⚙️ Config"
elif "full" in backup_file.name:
backup_type = "📦 Full"
else:
backup_type = "❓ Unknown"
print(f" {backup_type:12} | {backup_file.name:40} | {size_mb:6.1f}MB | {modified.strftime('%Y-%m-%d %H:%M')}")
def restore_backup(self, backup_file):
"""Restore from backup file"""
backup_path = Path(backup_file)
if not backup_path.exists():
# Try in backup directory
backup_path = self.backup_dir / backup_file
if not backup_path.exists():
print(f"❌ Backup file not found: {backup_file}")
return False
print(f"🔄 Restoring from backup: {backup_path.name}")
# Create restoration directory
restore_dir = self.app_root / f"restore_{self.get_timestamp()}"
restore_dir.mkdir(exist_ok=True)
try:
with tarfile.open(backup_path, "r:gz") as tar:
# Extract to restoration directory
tar.extractall(restore_dir)
# Check backup metadata
metadata_file = restore_dir / "backup_metadata.json"
if metadata_file.exists():
with open(metadata_file) as f:
metadata = json.load(f)
print(f" Backup type: {metadata.get('backup_type', 'unknown')}")
print(f" Created: {metadata.get('timestamp', 'unknown')}")
print(f" App version: {metadata.get('app_version', 'unknown')}")
# Ask for confirmation
confirm = input(" Proceed with restoration? (y/N): ").lower().strip()
if confirm != 'y':
shutil.rmtree(restore_dir)
print("❌ Restoration cancelled")
return False
# Restore files based on backup type
backup_type = metadata.get('backup_type', 'unknown') if metadata_file.exists() else 'unknown'
if backup_type == 'data':
# Restore data files
for data_file in self.data_files:
src = restore_dir / data_file
dst = self.app_root / data_file
if src.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
print(f" ✅ Restored: {data_file}")
elif backup_type == 'config':
# Restore config files
for config_file in self.config_files:
src = restore_dir / config_file
dst = self.app_root / config_file
if src.exists():
shutil.copy2(src, dst)
print(f" ✅ Restored: {config_file}")
elif backup_type == 'full':
# Full restoration - more complex
print(" ⚠️ Full restoration requires manual review")
print(f" Extracted to: {restore_dir}")
print(" Please manually copy files as needed")
return True
# Cleanup
shutil.rmtree(restore_dir)
print("✅ Restoration completed successfully")
return True
except Exception as e:
print(f"❌ Restoration failed: {e}")
if restore_dir.exists():
shutil.rmtree(restore_dir)
return False
def get_app_version(self):
"""Get application version from main.py or git"""
try:
# Try to get git commit hash
import subprocess
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
cwd=self.app_root,
capture_output=True,
text=True)
if result.returncode == 0:
return f"git-{result.stdout.strip()}"
except:
pass
# Fallback to file modification time
main_py = self.app_root / "main.py"
if main_py.exists():
mtime = datetime.fromtimestamp(main_py.stat().st_mtime)
return f"modified-{mtime.strftime('%Y%m%d')}"
return "unknown"
def cleanup_old_backups(self, keep_count=10):
"""Remove old backups, keeping only the most recent ones"""
backup_files = sorted(self.backup_dir.glob("*.tar.gz"),
key=lambda x: x.stat().st_mtime,
reverse=True)
if len(backup_files) <= keep_count:
return
print(f"🧹 Cleaning up old backups (keeping {keep_count} most recent)")
for backup_file in backup_files[keep_count:]:
print(f" 🗑️ Removing: {backup_file.name}")
backup_file.unlink()
def automated_backup(self):
"""Perform automated backup suitable for cron jobs"""
print(f"🤖 Automated backup started at {datetime.now()}")
try:
# Create data backup
self.create_data_backup()
# Create config backup weekly (if it's Monday)
if datetime.now().weekday() == 0: # Monday
self.create_config_backup()
# Create full backup monthly (if it's the 1st)
if datetime.now().day == 1:
self.create_full_backup()
# Cleanup old backups
self.cleanup_old_backups(keep_count=15)
print("✅ Automated backup completed successfully")
except Exception as e:
print(f"❌ Automated backup failed: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="QR Code Manager Backup Utility")
parser.add_argument("--data-only", action="store_true", help="Backup only data files")
parser.add_argument("--config", action="store_true", help="Backup only config files")
parser.add_argument("--full", action="store_true", help="Full backup including application files")
parser.add_argument("--restore", type=str, help="Restore from backup file")
parser.add_argument("--list", action="store_true", help="List available backups")
parser.add_argument("--auto", action="store_true", help="Automated backup (for cron jobs)")
parser.add_argument("--cleanup", type=int, help="Cleanup old backups, keeping N most recent")
args = parser.parse_args()
backup_util = QRCodeManagerBackup()
if args.list:
backup_util.list_backups()
elif args.restore:
backup_util.restore_backup(args.restore)
elif args.auto:
backup_util.automated_backup()
elif args.cleanup:
backup_util.cleanup_old_backups(args.cleanup)
elif args.config:
backup_util.create_config_backup()
elif args.full:
backup_util.create_full_backup()
else:
# Default: data backup
backup_util.create_data_backup()
if __name__ == "__main__":
main()