diff --git a/src/aipass/backup/apps/backup.py b/src/aipass/backup/apps/backup.py index a77f4c41..c7becc66 100755 --- a/src/aipass/backup/apps/backup.py +++ b/src/aipass/backup/apps/backup.py @@ -23,13 +23,13 @@ from aipass.cli.apps.modules import console, error from aipass.prax import logger # Explicit module imports (replaces dynamic discover_modules) -from aipass.backup.apps.modules import backup_core, google_drive_sync, integrations, reauth_drive +from aipass.backup.apps.modules import backup_core, google_drive_sync def get_modules(): """Return list of modules with handle_command() interface""" modules = [] - for mod in [backup_core, google_drive_sync, integrations, reauth_drive]: + for mod in [backup_core, google_drive_sync]: if hasattr(mod, 'handle_command'): modules.append(mod) return modules diff --git a/src/aipass/backup/apps/extensions/__init__.py b/src/aipass/backup/apps/extensions/__init__.py deleted file mode 100644 index 95322c94..00000000 --- a/src/aipass/backup/apps/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Extensions package - Drop-in extensions for branch functionality diff --git a/src/aipass/backup/apps/handlers/diff/version_manager.py b/src/aipass/backup/apps/handlers/diff/version_manager.py deleted file mode 100755 index 3f561c12..00000000 --- a/src/aipass/backup/apps/handlers/diff/version_manager.py +++ /dev/null @@ -1,140 +0,0 @@ -# =================== AIPass ==================== -# Name: version_manager.py -# Description: Version discovery and management -# Version: 2.0.0 -# Created: 2025-11-16 -# Modified: 2026-03-09 -# ============================================= - -""" -Version Manager Handler - -Discovers and manages versioned backup files using the file-organized structure. -Supports version listing and filtering. -""" - -# ============================================= -# IMPORTS -# ============================================= - -from aipass.prax import logger -from pathlib import Path -from typing import Dict, List, Optional - -# logger imported from aipass.prax - -# Import from handlers -from aipass.backup.apps.handlers.utils.system_utils import safe_print -from aipass.backup.apps.handlers.json import json_handler - -# ============================================= -# VERSION FILE DISCOVERY -# ============================================= - -def get_versioned_files(backup_path: Path, file_path: str | None = None) -> Dict[str, List[str]]: - """Get all versioned files using new file-organized structure. - - Args: - backup_path: Path to backup root directory - file_path: Optional filter to search for specific file - - Returns: - Dictionary mapping file paths to lists of version timestamps - """ - versioned_files = {} - - json_handler.log_operation("version_discovery") - if not backup_path.exists(): - return versioned_files - - # Normalize search path for cross-platform compatibility - normalized_search_path = None - if file_path: - normalized_search_path = str(Path(file_path)).replace('\\', '/').lower() - - # NEW STRUCTURE: Look for *_diffs folders - for diff_folder in backup_path.rglob('*_diffs'): - if not diff_folder.is_dir(): - continue - - # Extract base filename from diff folder name (remove _diffs suffix) - if not diff_folder.name.endswith('_diffs'): - continue - - base_filename = diff_folder.name[:-6] # Remove '_diffs' - - # Get relative path from backup root to the file - # diff_folder structure: .../filename/filename_diffs/ - file_folder = diff_folder.parent - if file_folder.name != base_filename: - continue # Skip if structure doesn't match expected pattern - - # Calculate base path for this file - rel_path_to_file_folder = file_folder.relative_to(backup_path) - base_path = str(rel_path_to_file_folder / base_filename) - - # Filter by search path if provided - if file_path: - normalized_base_path = str(base_path).replace('\\', '/').lower() - if normalized_search_path is not None and normalized_search_path not in normalized_base_path: - continue - - # Find all diff files in this folder - versions = [] - for diff_file in diff_folder.glob('*_v*.diff'): - filename = diff_file.name - if '_v' in filename and filename.endswith('.diff'): - # Extract version from filename: basename_v.diff - version_part = filename.split('_v')[1].replace('.diff', '') - versions.append(version_part) - - if versions: - versioned_files[base_path] = sorted(versions, reverse=True) # Newest first - - return versioned_files - - -def list_versioned_files(backup_path: Path) -> bool: - """List all versioned files with their version counts. - - Args: - backup_path: Path to backup root directory - - Returns: - True if files were found and listed, False otherwise - """ - try: - versioned_files = get_versioned_files(backup_path) - - if not versioned_files: - safe_print("No versioned files found") - return False - - safe_print(f"\n{'='*70}") - safe_print("VERSIONED FILES HISTORY") - safe_print('='*70) - - for file_path, versions in versioned_files.items(): - version_count = len(versions) - latest_version = versions[0] if versions else "unknown" - - safe_print(f" {file_path}") - safe_print(f" Versions: {version_count} | Latest: {latest_version}") - - # Show first 3 versions - for i, version in enumerate(versions[:3]): - marker = ">" if i == 0 else " " - safe_print(f" {marker} v{version}") - - if len(versions) > 3: - safe_print(f" ... and {len(versions) - 3} more versions") - safe_print("") - - safe_print('='*70) - safe_print(f"Use: python backup.py diff --file to see changes") - return True - - except Exception as e: - safe_print(f"Error listing versioned files: {e}") - logger.error(f"[version_manager] Failed to list versioned files: {e}") - return False diff --git a/src/aipass/backup/apps/handlers/diff/vscode_integration.py b/src/aipass/backup/apps/handlers/diff/vscode_integration.py deleted file mode 100755 index ae7bfef1..00000000 --- a/src/aipass/backup/apps/handlers/diff/vscode_integration.py +++ /dev/null @@ -1,223 +0,0 @@ -# =================== AIPass ==================== -# Name: vscode_integration.py -# Description: VS Code diff viewer integration -# Version: 2.0.0 -# Created: 2025-11-16 -# Modified: 2026-03-09 -# ============================================= - -""" -VS Code Integration Handler - -Opens file diffs in VS Code editor for visual comparison. -Supports baseline vs current and version-to-version comparisons. -""" - -# ============================================= -# IMPORTS -# ============================================= - -import sys -import os -import subprocess -from aipass.prax import logger -from pathlib import Path - -# logger imported from aipass.prax - -# Import from handlers -from aipass.backup.apps.handlers.utils.system_utils import safe_print -from aipass.backup.apps.handlers.diff.version_manager import get_versioned_files -from aipass.backup.apps.handlers.json import json_handler - -# ============================================= -# VS CODE INTEGRATION -# ============================================= - -def show_file_diff(backup_path: Path, source_dir: Path, file_path: str, version1: str | None = None, version2: str | None = None) -> bool: - """Show diff between two versions of a file in VS Code. - - Args: - backup_path: Path to backup root directory - source_dir: Path to source directory (for current file comparison) - file_path: File path to show diff for - version1: First version timestamp (optional) - version2: Second version timestamp (optional) - - Returns: - True if diff was opened successfully, False otherwise - """ - try: - json_handler.log_operation("vscode_diff_opened") - versioned_files = get_versioned_files(backup_path, file_path) - - if not versioned_files: - safe_print(f"No versioned files found for: {file_path}") - return False - - # Find the matching file with normalized path comparison - matching_file = None - normalized_search_path = str(Path(file_path)).replace('\\', '/').lower() - for base_path in versioned_files: - normalized_base_path = str(base_path).replace('\\', '/').lower() - if normalized_search_path in normalized_base_path: - matching_file = base_path - break - - if not matching_file: - safe_print(f"File not found: {file_path}") - return False - - versions = versioned_files[matching_file] - - if len(versions) < 1: - safe_print(f"Only one version found for: {file_path}") - return False - - # Determine which versions to compare - if version1 and version2: - if version1 not in versions or version2 not in versions: - safe_print(f"Version not found. Available: {', '.join(versions)}") - return False - else: - # Compare latest version with current file, or two most recent versions - current_file = backup_path / matching_file - if current_file.exists() and len(versions) >= 1: - version1 = "current" - version2 = versions[0] # Most recent version - elif len(versions) >= 2: - version1 = versions[1] # Second most recent - version2 = versions[0] # Most recent - else: - safe_print(f"Need at least 2 versions to compare") - return False - - # Determine file paths for comparison using NEW STRUCTURE - # For VS Code diff, we need actual files, not diff patches - try: - # NEW STRUCTURE: matching_file is like "root/CLAUDE.local.md/CLAUDE.local.md" - # We need to find the file folder and look for baseline + current file - - # Extract the base filename for pattern matching - base_filename = Path(matching_file).name - file_folder_path = backup_path / Path(matching_file).parent - - # Look for baseline file in the file folder - # Handle files with extensions like CLAUDE.local.md -> CLAUDE.local-baseline-* - name_without_ext = base_filename.rsplit('.', 1)[0] if '.' in base_filename else base_filename - baseline_pattern = f"{name_without_ext}-baseline-*" - baseline_files = list(file_folder_path.glob(baseline_pattern)) - - # Current backup file path (in the organized structure) - current_backup_file = file_folder_path / base_filename - - # Source file path (live file in source directory) - # Convert backup path back to source path - matching_file_normalized = str(Path(matching_file)).replace('\\', '/') - if matching_file_normalized.startswith("root/"): - # Root level file: root/filename/filename -> filename - source_relative_path = base_filename - else: - # Nested file: folder/filename/filename -> folder/filename - source_relative_path = str(Path(matching_file).parent.parent / base_filename) - - source_file_path = source_dir / source_relative_path - - if baseline_files: - # ALWAYS prefer baseline comparison when available - file1_path = baseline_files[0] # Use first baseline found - file2_path = source_file_path # Current source - label1 = f"{base_filename} (baseline)" - label2 = f"{base_filename} (current)" - elif current_backup_file.exists(): - # No baseline, compare backup against current source - file1_path = current_backup_file # Backup version - file2_path = source_file_path # Live source - label1 = f"{base_filename} (backup)" - label2 = f"{base_filename} (current)" - - # Check if they're the same - if file1_path.exists() and file2_path.exists(): - if file1_path.stat().st_mtime == file2_path.stat().st_mtime: - safe_print(f"Note: Backup is identical to current file (no changes)") - safe_print(f" This happens when Google Drive sync updates the backup") - else: - safe_print(f"Could not find backup file in new structure: {matching_file}") - return False - - # Verify files exist - if not file1_path.exists(): - safe_print(f"File not found: {file1_path}") - return False - if not file2_path.exists(): - safe_print(f"File not found: {file2_path}") - return False - - except Exception as e: - safe_print(f"Error locating files: {e}") - logger.error(f"[vscode_integration] Error locating files for diff: {e}") - return False - - # Open diff in VS Code - safe_print(f"\n{'='*70}") - safe_print(f"Opening diff in VS Code: {matching_file}") - safe_print(f"Comparing: {label2} -> {label1}") - safe_print('='*70) - - # Launch VS Code with diff view - try: - # Try to find VS Code command - code_cmd = 'code' - if sys.platform == 'win32': - # On Windows, try common VS Code paths if 'code' not in PATH - import shutil - if not shutil.which('code'): - # Platform-specific VS Code detection - if sys.platform == 'win32': - # Windows VS Code path - vscode_path = Path(r"C:\Users") / os.environ.get('USERNAME', 'input') / r"AppData\Local\Programs\Microsoft VS Code\bin\code.cmd" - if vscode_path.exists(): - code_cmd = str(vscode_path) - else: - code_cmd = 'code.cmd' - else: - # Linux - VS Code is usually in PATH or use snap - if Path('/snap/bin/code').exists(): - code_cmd = '/snap/bin/code' - elif Path('/usr/bin/code').exists(): - code_cmd = '/usr/bin/code' - else: - code_cmd = 'code' - - # Use subprocess for better error handling - # Swap order so baseline (older) is on left, current (newer) is on right - result = subprocess.run( - [code_cmd, '--diff', str(file1_path), str(file2_path)], - capture_output=True, - text=True, - shell=True if sys.platform == 'win32' else False - ) - if result.returncode == 0: - safe_print(f"Diff opened in VS Code") - else: - safe_print(f"VS Code returned code {result.returncode}") - if result.stderr: - safe_print(f"Error: {result.stderr}") - except FileNotFoundError: - safe_print(f"VS Code command not found. Make sure 'code' is in your PATH") - safe_print(f"You can manually compare:") - safe_print(f" File 1: {file2_path}") - safe_print(f" File 2: {file1_path}") - return False - except Exception as e: - safe_print(f"Error launching VS Code: {e}") - logger.error(f"[vscode_integration] Error launching VS Code: {e}") - return False - - safe_print('='*70) - return True - - except Exception as e: - safe_print(f"Error generating diff: {e}") - logger.error(f"[vscode_integration] Error in show_file_diff: {e}") - return False diff --git a/src/aipass/backup/apps/handlers/operations/integration_ops.py b/src/aipass/backup/apps/handlers/operations/integration_ops.py deleted file mode 100644 index cb9dd6aa..00000000 --- a/src/aipass/backup/apps/handlers/operations/integration_ops.py +++ /dev/null @@ -1,111 +0,0 @@ -# =================== AIPass ==================== -# Name: integration_ops.py -# Description: Backup Integration Operations Handler -# Version: 1.0.0 -# Created: 2026-02-20 -# Modified: 2026-03-09 -# ============================================= - -""" -Backup Integration Operations Handler - -Implementation logic for external integrations: -- Google Drive sync operations -- Backup directory read-only protection - -Called by the integrations module orchestrator. -""" - -import os -import stat -from pathlib import Path -from typing import Any, Optional - -from aipass.backup.apps.handlers.json import json_handler - - -def sync_to_drive( - backup_path: Path, - source_dir: Path, - mode: str, - backup_note: str, - drive_sync_module: Any, - drive_sync_available: bool, -) -> bool: - """Sync versioned backups to Google Drive. - - Args: - backup_path: Path to backup directory to sync - source_dir: Source directory being backed up (for project name) - mode: Backup mode ('snapshot' or 'versioned') - backup_note: Optional note describing the backup - drive_sync_module: The GoogleDriveSync class (injected) - drive_sync_available: Whether google_drive_sync is importable - - Returns: - bool: True if sync succeeded, False otherwise - """ - json_handler.log_operation("drive_sync_initiated") - - if not drive_sync_available or drive_sync_module is None: - return False - - if mode != 'versioned': - return False - - try: - project_name = "AIPass" - drive_sync = drive_sync_module() - - if not drive_sync.authenticate(): - return False - - result = drive_sync.sync_backup_files( - backup_dir=backup_path, - project_name=project_name, - note=backup_note - ) - return result["success"] if isinstance(result, dict) else result - - except Exception: - return False - - -def set_backup_readonly(backup_path: Path) -> tuple[bool, str]: - """Set backup directory to read-only for protection. - - Applies read-only permissions recursively to all directories and files. - - Args: - backup_path: Path to backup directory to protect - - Returns: - Tuple of (success: bool, message: str) - """ - if not backup_path.exists(): - return False, f"Backup path does not exist: {backup_path}" - - try: - protected_dirs = 0 - protected_files = 0 - - for root, dirs, files in os.walk(str(backup_path)): - try: - os.chmod(root, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | - stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - protected_dirs += 1 - except Exception: - pass - - for file in files: - file_path = os.path.join(root, file) - try: - os.chmod(file_path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) - protected_files += 1 - except Exception: - pass - - return True, f"Protected {protected_dirs} dirs, {protected_files} files" - - except Exception as e: - return False, f"Could not set read-only protection: {e}" diff --git a/src/aipass/backup/apps/handlers/utils/backup_timestamps.py b/src/aipass/backup/apps/handlers/utils/backup_timestamps.py index d16db914..6c8c66fe 100644 --- a/src/aipass/backup/apps/handlers/utils/backup_timestamps.py +++ b/src/aipass/backup/apps/handlers/utils/backup_timestamps.py @@ -20,7 +20,7 @@ from datetime import datetime from aipass.backup.apps.handlers.json import json_handler _BACKUP_ROOT = Path(__file__).resolve().parents[3] # src/aipass/backup/ -TIMESTAMPS_FILE = _BACKUP_ROOT / "backup_data" / "backup_timestamps.json" +TIMESTAMPS_FILE = _BACKUP_ROOT / "backup_json" / "backup_timestamps.json" MODES = ["snapshot", "versioned", "drive_sync"] diff --git a/src/aipass/backup/apps/handlers/utils/reauth_handler.py b/src/aipass/backup/apps/handlers/utils/reauth_handler.py deleted file mode 100644 index fe9ee0d9..00000000 --- a/src/aipass/backup/apps/handlers/utils/reauth_handler.py +++ /dev/null @@ -1,80 +0,0 @@ -# =================== AIPass ==================== -# Name: reauth_handler.py -# Description: Google Drive Re-Authentication Handler -# Version: 1.0.0 -# Created: 2026-02-20 -# Modified: 2026-03-09 -# ============================================= - -""" -Google Drive Re-Authentication Handler - -Implementation logic for re-authenticating with Google Drive via console OAuth flow. -Called by the reauth_drive module orchestrator. -""" - -from pathlib import Path - -from aipass.backup.apps.handlers.json import json_handler - -SCOPES = ['https://www.googleapis.com/auth/drive.file'] -CREDS_PATH = Path.home() / '.aipass' / 'drive_creds.json' - - -def reauth(client_secrets_path: Path) -> bool: - """Perform Google Drive re-authentication via console OAuth flow. - - Args: - client_secrets_path: Path to the OAuth client secrets JSON file - - Returns: - bool: True if authentication succeeded, False otherwise - """ - try: - from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore[import-unresolved] - from google.oauth2.credentials import Credentials # type: ignore[import-unresolved] - from google.auth.transport.requests import Request # type: ignore[import-unresolved] - from googleapiclient.discovery import build # type: ignore[import-unresolved] - except ImportError as e: - return False - - json_handler.log_operation("reauth_initiated") - - # Step 1: Try refreshing existing token first - if CREDS_PATH.exists(): - try: - creds = Credentials.from_authorized_user_file(str(CREDS_PATH), SCOPES) - if creds and creds.expired and creds.refresh_token: - creds.refresh(Request()) - with open(CREDS_PATH, 'w', encoding='utf-8') as f: - f.write(creds.to_json()) - # Test connection - service = build('drive', 'v3', credentials=creds) - about = service.about().get(fields="user").execute() - return True - except Exception: - pass # Proceed with full re-authentication - - # Step 2: Full OAuth flow via console - if not client_secrets_path.exists(): - return False - - flow = InstalledAppFlow.from_client_secrets_file(str(client_secrets_path), SCOPES) - - try: - creds = flow.run_local_server(port=8085, open_browser=False) - except Exception: - return False - - # Save new credentials - CREDS_PATH.parent.mkdir(parents=True, exist_ok=True) - with open(CREDS_PATH, 'w', encoding='utf-8') as f: - f.write(creds.to_json()) - - # Test connection - try: - service = build('drive', 'v3', credentials=creds) - about = service.about().get(fields="user,storageQuota").execute() - return True - except Exception: - return False diff --git a/src/aipass/backup/apps/modules/backup_core.py b/src/aipass/backup/apps/modules/backup_core.py index 050b5183..bda5cb09 100755 --- a/src/aipass/backup/apps/modules/backup_core.py +++ b/src/aipass/backup/apps/modules/backup_core.py @@ -106,7 +106,7 @@ def print_introspection(): console.print(f" [cyan]handlers/{domain}/[/cyan]") console.print() - console.print("[dim]Run 'python3 backup_core.py --help' for usage[/dim]") + console.print("[dim]Run 'drone @backup --help' for usage[/dim]") console.print() @@ -123,8 +123,8 @@ def print_help(): console.print("Main backup orchestration module") console.print() console.print("[yellow]Usage:[/yellow]") - console.print(" python3 backup_core.py # Show module info") - console.print(" python3 backup_core.py --help # Show this help") + console.print(" drone @backup # Show module info") + console.print(" drone @backup --help # Show this help") console.print() console.print("[yellow]Commands:[/yellow]") console.print(" snapshot Full copy backup (overwrites destination)") @@ -153,6 +153,10 @@ def handle_command(args) -> bool: if not hasattr(args, 'command'): return False + if args.command in ['--help', '-h', 'help']: + print_help() + return True + # Check for backup commands ('all' handled by entry point, listed here for drone discovery) if args.command not in ['snapshot', 'versioned', 'all']: return False diff --git a/src/aipass/backup/apps/modules/integrations.py b/src/aipass/backup/apps/modules/integrations.py deleted file mode 100755 index 461c6e0e..00000000 --- a/src/aipass/backup/apps/modules/integrations.py +++ /dev/null @@ -1,291 +0,0 @@ -# =================== AIPass ==================== -# Name: integrations.py -# Description: External integrations and backup protection -# Version: 2.1.0 -# Created: 2025-11-29 -# Modified: 2026-03-09 -# ============================================= - -""" -Backup System Integrations Module - -Handles external integrations for the backup system following seed architecture standards. -Provides CLI command routing and integration orchestration. - -This module manages optional features that integrate with external services -or provide additional protection mechanisms for backups: -- Google Drive cloud synchronization (optional) -- Backup directory protection (read-only permissions) - -Architecture Pattern: -- handle_command(args) - CLI entry point for integration commands -- Integration functions with proper error handling and logging -- Graceful degradation for optional dependencies (Google Drive) -""" - -# ============================================= -# IMPORTS -# ============================================= - -import sys -import os -import stat -from pathlib import Path - -from aipass.cli.apps.modules import console -from aipass.prax import logger -from aipass.backup.apps.handlers.json import json_handler - - -def _header(text): - console.print(f"\n[bold cyan]{'='*70}[/bold cyan]") - console.print(f"[bold cyan] {text}[/bold cyan]") - console.print(f"[bold cyan]{'='*70}[/bold cyan]") - - -# Handler imports - relative imports -from aipass.backup.apps.handlers.utils.system_utils import safe_print -from aipass.backup.apps.handlers.operations.integration_ops import ( - sync_to_drive as _sync_to_drive_handler, - set_backup_readonly as _set_backup_readonly_handler, -) - -# Google Drive sync integration (optional - graceful degradation if not available) -try: - from aipass.backup.apps.modules.google_drive_sync import GoogleDriveSync - DRIVE_SYNC_AVAILABLE = True -except ImportError: - GoogleDriveSync = None # type: ignore - DRIVE_SYNC_AVAILABLE = False - logger.info("[integrations] Google Drive sync module not available - sync features disabled") - -# ============================================= -# MODULE-LEVEL COMMAND HANDLER -# ============================================= - - -def handle_command(args) -> bool | None: - """Route integration commands to appropriate handler. - - This is the module-level entry point for CLI commands related to integrations. - Routes integration-specific commands like sync-to-drive and set-readonly. - - Args: - args: Command-line arguments from CLI parser - - Returns: - bool: True if command succeeded, False otherwise, None if command not matched - - Example: - result = handle_command(args) - if result: - console.print("Integration command completed successfully") - - Supported commands: - - sync-to-drive: Sync backup to Google Drive - - set-readonly: Protect backup with read-only permissions - """ - if not args: - print_introspection() - return True - - if not hasattr(args, 'integration_command'): - return None - - json_handler.log_operation("integration_command") - - if args.integration_command == 'sync-to-drive': - backup_path = Path(args.backup_path) if hasattr(args, 'backup_path') else None - source_dir = Path(args.source_dir) if hasattr(args, 'source_dir') else None - mode = getattr(args, 'mode', 'versioned') - backup_note = getattr(args, 'backup_note', '') - - if not backup_path or not source_dir: - logger.warning("[integrations] Missing required arguments for sync-to-drive") - return False - - return _sync_to_drive(backup_path, source_dir, mode, backup_note) - - elif args.integration_command == 'set-readonly': - backup_path = Path(args.backup_path) if hasattr(args, 'backup_path') else None - if not backup_path: - logger.warning("[integrations] Missing backup_path argument for set-readonly") - return False - - return _set_backup_readonly(backup_path) - - return None - -# ============================================= -# GOOGLE DRIVE INTEGRATION -# ============================================= - - -def _sync_to_drive(backup_path: Path, source_dir: Path, mode: str, backup_note: str = "") -> bool: - """Orchestrate sync to Google Drive using integration handler. - - Args: - backup_path: Path to backup directory to sync - source_dir: Source directory being backed up - mode: Backup mode ('snapshot' or 'versioned') - backup_note: Optional note describing the backup - - Returns: - bool: True if sync succeeded, False otherwise - """ - if not DRIVE_SYNC_AVAILABLE: - safe_print("\033[93m[WARNING] Google Drive sync module not available - check backup_system installation\033[0m") - logger.warning("[integrations] Drive sync requested but google_drive_sync module not found") - return False - - if mode != 'versioned': - safe_print("[INFO] Drive sync only available for versioned backups") - logger.info(f"[integrations] Drive sync skipped - mode {mode} not supported (versioned only)") - return False - - try: - # Display sync header - safe_print("\n" + "="*70) - safe_print("\033[96m GOOGLE DRIVE SYNC\033[0m") - safe_print("="*70) - - project_name = "AIPass" - safe_print(f"\033[92m✓\033[0m Google Drive folder name: \033[1m{project_name}\033[0m") - safe_print(f"\033[92m✓\033[0m Destination: Google Drive (versioned backups)") - safe_print("-"*70) - - logger.info("[integrations] Starting Google Drive sync") - safe_print("\033[94m[AUTH]\033[0m Verifying Google Drive credentials...") - - success = _sync_to_drive_handler( - backup_path=backup_path, - source_dir=source_dir, - mode=mode, - backup_note=backup_note, - drive_sync_module=GoogleDriveSync, - drive_sync_available=DRIVE_SYNC_AVAILABLE, - ) - - if success: - safe_print("\033[92m✓ Sync completed successfully\033[0m") - logger.info(f"[integrations] Drive sync completed successfully for {project_name}") - else: - logger.error("[integrations] Drive sync completed with errors") - return success - - except Exception as e: - error_msg = f"Drive sync failed: {e}" - safe_print(f"[ERROR] {error_msg}") - logger.error(f"[integrations] {error_msg}") - return False - - - -# ============================================= -# FILE PROTECTION -# ============================================= - - -def _set_backup_readonly(backup_path: Path) -> bool: - """Orchestrate backup directory read-only protection using handler. - - Args: - backup_path: Path to backup directory to protect - - Returns: - bool: True if protection applied successfully, False otherwise - """ - try: - if not backup_path.exists(): - warning_msg = f"Backup path does not exist: {backup_path}" - safe_print(f"[WARNING] {warning_msg}") - logger.warning(f"[integrations] {warning_msg}") - return False - - success, message = _set_backup_readonly_handler(backup_path) - - if success: - safe_print(f"[PROTECTION] Backup directory set to read-only: {backup_path}") - logger.info(f"[integrations] Read-only set: {backup_path} ({message})") - else: - safe_print(f"[WARNING] {message}") - logger.warning(f"[integrations] {message}") - return success - - except Exception as e: - warning_msg = f"Could not set read-only protection: {e}" - safe_print(f"[WARNING] {warning_msg}") - logger.warning(f"[integrations] {warning_msg}") - return False - - - -# ============================================= -# MODULE INITIALIZATION -# ============================================= - -# Log module initialization -logger.info("[integrations] Module loaded - external integration support ready") -if DRIVE_SYNC_AVAILABLE: - logger.info("[integrations] Google Drive sync available") -else: - logger.info("[integrations] Google Drive sync unavailable - install google_drive_sync.py to enable") - -# ============================================= -# MAIN ENTRY POINT -# ============================================= - -def print_help(): - """Display help information for the integrations module.""" - _header("BACKUP SYSTEM - INTEGRATIONS MODULE") - safe_print("") - safe_print("\033[1mPURPOSE:\033[0m") - safe_print(" External integrations for the backup system") - safe_print("") - safe_print("\033[1mAVAILABLE SUBCOMMANDS:\033[0m") - safe_print("") - safe_print(" \033[92msync-to-drive\033[0m") - safe_print(" Sync versioned backups to Google Drive cloud storage") - safe_print(" Requires: backup_path, source_dir, mode, backup_note") - safe_print(" Status: " + ("\033[92mAvailable\033[0m" if DRIVE_SYNC_AVAILABLE else "\033[93mUnavailable (google_drive_sync module not found)\033[0m")) - safe_print("") - safe_print(" \033[92mset-readonly\033[0m") - safe_print(" Protect backup directory with read-only permissions") - safe_print(" Requires: backup_path") - safe_print(" Status: \033[92mAvailable\033[0m") - safe_print("") - safe_print("\033[1mUSAGE:\033[0m") - safe_print(" This module is called via the backup system CLI:") - safe_print(" \033[90m$ backup integration sync-to-drive --backup-path ...\033[0m") - safe_print(" \033[90m$ backup integration set-readonly --backup-path \033[0m") - safe_print("") - safe_print("\033[1mOR\033[0m import functions directly in Python:") - safe_print(" \033[90mfrom aipass.backup.apps.modules.integrations import handle_command\033[0m") - safe_print("") - safe_print("-"*70) - safe_print("\033[1mCommands:\033[0m sync-to-drive, set-readonly") - safe_print("="*70) - safe_print("") - -def print_introspection(): - """Display module introspection info.""" - console.print() - console.print("integrations Module") - console.print("External integrations and backup protection (Drive sync, read-only)") - console.print() - console.print("Connected Handlers:") - console.print(" handlers/utils/") - console.print(" - system_utils.py (safe_print — terminal-safe output wrapper)") - console.print(" handlers/operations/") - console.print(" - integration_ops.py (sync_to_drive — Drive upload orchestration)") - console.print(" - integration_ops.py (set_backup_readonly — read-only permission setter)") - console.print() - - -if __name__ == "__main__": - """Display help when module is run directly.""" - import sys as _sys - if len(_sys.argv) > 1 and _sys.argv[1] in ['--help', '-h', 'help']: - print_help() - else: - print_help() diff --git a/src/aipass/backup/apps/modules/reauth_drive.py b/src/aipass/backup/apps/modules/reauth_drive.py deleted file mode 100644 index e25a5dcf..00000000 --- a/src/aipass/backup/apps/modules/reauth_drive.py +++ /dev/null @@ -1,152 +0,0 @@ -# =================== AIPass ==================== -# Name: reauth_drive.py -# Description: Google Drive Re-Authentication Module -# Version: 1.2.0 -# Created: 2026-02-21 -# Modified: 2026-03-09 -# ============================================= - -""" -reauth_drive - Google Drive Re-Authentication Module - -Standalone Google Drive re-authentication module. -Uses console-based OAuth flow (no browser needed). -Delegates implementation to reauth_handler. -""" - -import sys -from pathlib import Path - -from aipass.cli.apps.modules import console -from aipass.cli.apps.modules.display import error -from aipass.prax import logger -from aipass.backup.apps.handlers.json import json_handler - -# Handler imports -from aipass.backup.apps.handlers.utils.reauth_handler import reauth as _run_reauth - -CLIENT_SECRETS = Path(__file__).resolve().parents[1] / 'credentials.json' -CREDS_PATH = Path.home() / '.aipass' / 'drive_creds.json' - - -def handle_command(args) -> bool: - """Handle reauth commands routed from the backup_system orchestrator. - - Args: - args: Command-line arguments from CLI parser - - Returns: - bool: True if command handled, False otherwise - """ - if not args: - print_introspection() - return True - - if not hasattr(args, 'command'): - return False - - command = args.command - if command in ['--help', '-h', 'help']: - print_help() - return True - - if command == 'reauth': - json_handler.log_operation("reauth_command") - return _execute_reauth() - - return False - - -def print_help(): - """Display help for the reauth_drive module.""" - console.print() - console.print("[bold cyan]reauth_drive - Google Drive Re-Authentication[/bold cyan]") - console.print() - console.print("Re-authenticates with Google Drive using console OAuth flow.") - console.print() - console.print("[yellow]Usage:[/yellow]") - console.print(" python3 reauth_drive.py # Run re-authentication") - console.print(" python3 reauth_drive.py --help # Show this help") - console.print() - - -def _execute_reauth() -> bool: - """Orchestrate the re-authentication flow with user feedback.""" - try: - from google.oauth2.credentials import Credentials - from google.auth.transport.requests import Request # noqa: F811 - from googleapiclient.discovery import build - except ImportError as e: - error(f"Missing package: {e}") - console.print(f"Install: {sys.executable} -m pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib") - return False - - if not CLIENT_SECRETS.exists(): - error(f"Client secrets not found at: {CLIENT_SECRETS}") - logger.warning(f"OAuth client secrets not found at: {CLIENT_SECRETS}") - return False - - console.print() - console.print("[bold cyan]" + "="*60 + "[/bold cyan]") - console.print("[bold cyan]GOOGLE DRIVE RE-AUTHENTICATION[/bold cyan]") - console.print("[bold cyan]" + "="*60 + "[/bold cyan]") - - # Check for token refresh first - if CREDS_PATH.exists(): - try: - creds = Credentials.from_authorized_user_file(str(CREDS_PATH), ['https://www.googleapis.com/auth/drive.file']) - if creds and creds.expired and creds.refresh_token: - console.print("Attempting token refresh...") - logger.info("Attempting Drive token refresh") - except Exception as e: - logger.warning(f"Token refresh check failed: {e}") - - console.print() - console.print("Starting local auth server...") - console.print("A URL will be printed below - open it in your browser.") - console.print("After authorizing, the browser will redirect to localhost.") - console.print() - - success = _run_reauth(CLIENT_SECRETS) - - if success: - console.print("[green]Re-authentication SUCCESSFUL![/green]") - logger.info("Drive re-authentication successful") - # Show account info - try: - creds = Credentials.from_authorized_user_file(str(CREDS_PATH), ['https://www.googleapis.com/auth/drive.file']) - service = build('drive', 'v3', credentials=creds) - about = service.about().get(fields="user,storageQuota").execute() - email = about['user']['emailAddress'] - quota = about.get('storageQuota', {}) - usage_gb = int(quota.get('usage', 0)) / (1024**3) - limit_gb = int(quota.get('limit', 0)) / (1024**3) - console.print(f"Authenticated as: {email}") - console.print(f"Storage: {usage_gb:.2f} GB / {limit_gb:.2f} GB") - except Exception: - pass - else: - error("Re-authentication FAILED") - logger.error("Drive re-authentication failed") - - return success - - -def print_introspection(): - """Display module introspection info.""" - console.print() - console.print("reauth_drive Module") - console.print("Google Drive re-authentication via console OAuth flow") - console.print() - console.print("Connected Handlers:") - console.print(" handlers/utils/") - console.print(" - reauth_handler.py (reauth — OAuth credential refresh and re-auth)") - console.print() - - -if __name__ == "__main__": - if len(sys.argv) > 1 and sys.argv[1] in ['--help', '-h', 'help']: - print_help() - sys.exit(0) - success = _execute_reauth() - sys.exit(0 if success else 1) diff --git a/src/aipass/backup/apps/plugins/__init__.py b/src/aipass/backup/apps/plugins/__init__.py index 69b056dd..e69de29b 100644 --- a/src/aipass/backup/apps/plugins/__init__.py +++ b/src/aipass/backup/apps/plugins/__init__.py @@ -1 +0,0 @@ -# Plugins package - Pluggable components for branch capabilities diff --git a/src/aipass/backup/docs/README.md b/src/aipass/backup/docs/README.md new file mode 100644 index 00000000..dcf2c804 --- /dev/null +++ b/src/aipass/backup/docs/README.md @@ -0,0 +1 @@ +# Placeholder diff --git a/src/aipass/backup/processed_plans/TPLAN-0001_test_tplan_auto_discovery_2026-03-18.md b/src/aipass/backup/processed_plans/TPLAN-0001_test_tplan_auto_discovery_2026-03-18.md deleted file mode 100644 index 3f06bb69..00000000 --- a/src/aipass/backup/processed_plans/TPLAN-0001_test_tplan_auto_discovery_2026-03-18.md +++ /dev/null @@ -1,9 +0,0 @@ -# TPLAN-0001: Test TPLAN auto-discovery - -Tag: - -> Test template for auto-discovery verification - ---- - -*Created: 2026-03-18* diff --git a/src/aipass/backup/processed_plans/TPLAN-0001_test_tplan_via_registry_2026-03-18.md b/src/aipass/backup/processed_plans/TPLAN-0001_test_tplan_via_registry_2026-03-18.md deleted file mode 100644 index 0fedd202..00000000 --- a/src/aipass/backup/processed_plans/TPLAN-0001_test_tplan_via_registry_2026-03-18.md +++ /dev/null @@ -1,13 +0,0 @@ -# TPLAN-0001: Test TPLAN via registry - -Tag: - -> Testing template — auto-discovered from filesystem - ---- - -## Notes - ---- -*Created: 2026-03-18* -*Updated: 2026-03-18*