EU-Utility/core/backup.py

263 lines
8.6 KiB
Python

"""
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