| """ |
| PhD Research OS — Backup & Recovery (Phase 6) |
| =============================================== |
| Automated backup and restore for SQLite database. |
| """ |
|
|
| import os |
| import sqlite3 |
| import shutil |
| from datetime import datetime |
| from pathlib import Path |
|
|
|
|
| def backup_database(db_path: str = "data/research_os.db", |
| backup_dir: str = "data/backups") -> str: |
| """ |
| Create a timestamped backup of the database using SQLite's backup API. |
| Returns the backup file path. |
| """ |
| os.makedirs(backup_dir, exist_ok=True) |
| |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
| backup_path = os.path.join(backup_dir, f"research_os_{timestamp}.db") |
| |
| |
| source = sqlite3.connect(db_path) |
| dest = sqlite3.connect(backup_path) |
| source.backup(dest) |
| source.close() |
| dest.close() |
| |
| |
| conn = sqlite3.connect(backup_path) |
| claim_count = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0] |
| conn.close() |
| |
| print(f"Backup created: {backup_path} ({claim_count} claims)") |
| return backup_path |
|
|
|
|
| def restore_database(backup_path: str, db_path: str = "data/research_os.db") -> bool: |
| """ |
| Restore database from backup. |
| Creates a safety backup of current DB first. |
| """ |
| if not os.path.exists(backup_path): |
| print(f"Error: Backup file not found: {backup_path}") |
| return False |
| |
| |
| if os.path.exists(db_path): |
| safety = db_path + ".pre_restore" |
| shutil.copy2(db_path, safety) |
| print(f"Safety backup: {safety}") |
| |
| |
| source = sqlite3.connect(backup_path) |
| dest = sqlite3.connect(db_path) |
| source.backup(dest) |
| source.close() |
| dest.close() |
| |
| |
| conn = sqlite3.connect(db_path) |
| claim_count = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0] |
| conn.close() |
| |
| print(f"Restored from {backup_path} ({claim_count} claims)") |
| return True |
|
|
|
|
| def list_backups(backup_dir: str = "data/backups") -> list: |
| """List available backups with metadata.""" |
| if not os.path.exists(backup_dir): |
| return [] |
| |
| backups = [] |
| for f in sorted(Path(backup_dir).glob("research_os_*.db"), reverse=True): |
| conn = sqlite3.connect(str(f)) |
| try: |
| claim_count = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0] |
| except: |
| claim_count = -1 |
| conn.close() |
| |
| backups.append({ |
| "path": str(f), |
| "size_mb": f.stat().st_size / 1024 / 1024, |
| "claims": claim_count, |
| "created": datetime.fromtimestamp(f.stat().st_mtime).isoformat() |
| }) |
| |
| return backups |
|
|