""" EU-Utility - Backup Manager (Core Framework Component) Built-in backup/import/export - not a plugin. """ import json import shutil import zipfile from pathlib import Path from datetime import datetime from typing import Dict, List, Optional from dataclasses import dataclass @dataclass class BackupInfo: """Backup information.""" path: Path name: str date: datetime size: int version: str class BackupManager: """Backup manager - built into the framework. Handles import, export, and restore of EU-Utility data. """ BACKUP_DIR = Path("backups") DATA_DIR = Path("data") CONFIG_DIR = Path("config") def __init__(self): self.BACKUP_DIR.mkdir(exist_ok=True) def create_backup(self, name: Optional[str] = None) -> Path: """Create a backup of all EU-Utility data. Args: name: Optional backup name (defaults to timestamp) Returns: Path to created backup file """ if name is None: name = f"eu_utility_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" backup_path = self.BACKUP_DIR / f"{name}.zip" with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zf: # Backup data directory if self.DATA_DIR.exists(): for file_path in self.DATA_DIR.rglob('*'): if file_path.is_file(): arcname = f"data/{file_path.relative_to(self.DATA_DIR)}" zf.write(file_path, arcname) # Backup config directory if self.CONFIG_DIR.exists(): for file_path in self.CONFIG_DIR.rglob('*'): if file_path.is_file(): arcname = f"config/{file_path.relative_to(self.CONFIG_DIR)}" zf.write(file_path, arcname) # Add metadata metadata = { 'version': '2.1.0', 'created': datetime.now().isoformat(), 'name': name } zf.writestr('metadata.json', json.dumps(metadata, indent=2)) return backup_path def restore_backup(self, backup_path: Path) -> bool: """Restore from a backup file. Args: backup_path: Path to backup zip file Returns: True if successful """ try: with zipfile.ZipFile(backup_path, 'r') as zf: # Validate backup if 'metadata.json' not in zf.namelist(): raise ValueError("Invalid backup file: missing metadata") # Extract for member in zf.namelist(): if member.startswith('data/'): zf.extract(member, '.') elif member.startswith('config/'): zf.extract(member, '.') return True except Exception as e: print(f"[BackupManager] Restore failed: {e}") return False def list_backups(self) -> List[BackupInfo]: """List all available backups. Returns: List of backup information """ backups = [] for backup_file in self.BACKUP_DIR.glob('*.zip'): try: stat = backup_file.stat() # Try to read metadata name = backup_file.stem version = "unknown" date = datetime.fromtimestamp(stat.st_mtime) with zipfile.ZipFile(backup_file, 'r') as zf: if 'metadata.json' in zf.namelist(): metadata = json.loads(zf.read('metadata.json')) name = metadata.get('name', name) version = metadata.get('version', version) date = datetime.fromisoformat(metadata.get('created', date.isoformat())) backups.append(BackupInfo( path=backup_file, name=name, date=date, size=stat.st_size, version=version )) except Exception as e: print(f"[BackupManager] Error reading backup {backup_file}: {e}") # Sort by date (newest first) backups.sort(key=lambda b: b.date, reverse=True) return backups def delete_backup(self, backup_path: Path) -> bool: """Delete a backup file. Args: backup_path: Path to backup file Returns: True if deleted """ try: backup_path.unlink() return True except Exception as e: print(f"[BackupManager] Delete failed: {e}") return False def export_data(self, export_path: Path, include_config: bool = True) -> bool: """Export data to a specific path. Args: export_path: Path to export to include_config: Whether to include config files Returns: True if successful """ try: with zipfile.ZipFile(export_path, 'w', zipfile.ZIP_DEFLATED) as zf: # Export data if self.DATA_DIR.exists(): for file_path in self.DATA_DIR.rglob('*'): if file_path.is_file(): zf.write(file_path, f"data/{file_path.relative_to(self.DATA_DIR)}") # Export config if include_config and self.CONFIG_DIR.exists(): for file_path in self.CONFIG_DIR.rglob('*'): if file_path.is_file(): zf.write(file_path, f"config/{file_path.relative_to(self.CONFIG_DIR)}") # Add manifest manifest = { 'type': 'eu_utility_export', 'version': '2.1.0', 'exported': datetime.now().isoformat(), 'include_config': include_config } zf.writestr('manifest.json', json.dumps(manifest, indent=2)) return True except Exception as e: print(f"[BackupManager] Export failed: {e}") return False def import_data(self, import_path: Path, merge: bool = False) -> bool: """Import data from a file. Args: import_path: Path to import file merge: If True, merge with existing data. If False, replace. Returns: True if successful """ try: with zipfile.ZipFile(import_path, 'r') as zf: # Validate if 'manifest.json' not in zf.namelist(): raise ValueError("Invalid import file") manifest = json.loads(zf.read('manifest.json')) if manifest.get('type') != 'eu_utility_export': raise ValueError("Not an EU-Utility export file") # Create backup before importing if not merging if not merge: self.create_backup("pre_import_backup") # Extract for member in zf.namelist(): if member in ['manifest.json', 'metadata.json']: continue # Merge or replace logic could go here zf.extract(member, '.') return True except Exception as e: print(f"[BackupManager] Import failed: {e}") return False def clear_all_data(self) -> bool: """Clear all EU-Utility data. Returns: True if successful """ try: # Create backup first self.create_backup("pre_clear_backup") # Clear data directory if self.DATA_DIR.exists(): shutil.rmtree(self.DATA_DIR) self.DATA_DIR.mkdir() # Clear config (optional - keep some settings?) # if self.CONFIG_DIR.exists(): # shutil.rmtree(self.CONFIG_DIR) return True except Exception as e: print(f"[BackupManager] Clear failed: {e}") return False