263 lines
8.6 KiB
Python
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
|