#!/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()