diff --git a/src/aipass/backup/README.md b/src/aipass/backup/README.md index 4b1f9f4c..db9b7af9 100644 --- a/src/aipass/backup/README.md +++ b/src/aipass/backup/README.md @@ -4,7 +4,7 @@ **Module:** `aipass.backup` **Created:** 2026-03-07 **Citizen Class:** builder -**Last Updated:** 2026-03-08 +**Last Updated:** 2026-03-17 --- diff --git a/src/aipass/backup/apps/handlers/config/config_handler.py b/src/aipass/backup/apps/handlers/config/config_handler.py index 655c85a0..541ff231 100755 --- a/src/aipass/backup/apps/handlers/config/config_handler.py +++ b/src/aipass/backup/apps/handlers/config/config_handler.py @@ -25,6 +25,7 @@ from pathlib import Path from typing import Dict, Set, List, Optional # Re-export from ignore_patterns for backwards compatibility +from aipass.backup.apps.handlers.json import json_handler from aipass.backup.apps.handlers.config.ignore_patterns import ( GLOBAL_IGNORE_PATTERNS, IGNORE_EXCEPTIONS, should_ignore, filter_tracked_items, get_ignore_patterns, get_cli_tracking_patterns, @@ -82,6 +83,7 @@ def get_backup_destination(system_name: str) -> str: Returns: Path to backup destination, or base directory if not found """ + json_handler.log_operation("config_loaded") return BACKUP_DESTINATIONS.get(system_name, BASE_BACKUP_DIR) # ============================================= diff --git a/src/aipass/backup/apps/handlers/config/ignore_patterns.py b/src/aipass/backup/apps/handlers/config/ignore_patterns.py index 492abdda..30e8ab99 100644 --- a/src/aipass/backup/apps/handlers/config/ignore_patterns.py +++ b/src/aipass/backup/apps/handlers/config/ignore_patterns.py @@ -25,6 +25,7 @@ from pathlib import Path from typing import Dict, Set, List, Optional from aipass.prax import logger +from aipass.backup.apps.handlers.json import json_handler # ============================================= # PATTERN LOADING @@ -53,6 +54,7 @@ def load_patterns() -> dict: with open(_PATTERNS_JSON, "r", encoding="utf-8") as f: data = json.load(f) + json_handler.log_operation("patterns_loaded") logger.info(f"[ignore_patterns] Loaded patterns from {_PATTERNS_JSON}") return data diff --git a/src/aipass/backup/apps/handlers/diff/diff_generator.py b/src/aipass/backup/apps/handlers/diff/diff_generator.py index b88b112e..92703ee3 100755 --- a/src/aipass/backup/apps/handlers/diff/diff_generator.py +++ b/src/aipass/backup/apps/handlers/diff/diff_generator.py @@ -24,6 +24,7 @@ from pathlib import Path # Import from handlers from aipass.backup.apps.handlers.utils.system_utils import safe_print from aipass.backup.apps.handlers.config.config_handler import DIFF_IGNORE_PATTERNS, DIFF_INCLUDE_PATTERNS +from aipass.backup.apps.handlers.json import json_handler # ============================================= # DIFF GENERATION @@ -101,6 +102,7 @@ def generate_diff_content(old_file: Path, new_file: Path) -> str: lineterm='' ) + json_handler.log_operation("diff_generated") return '\n'.join(diff_lines) except Exception as e: diff --git a/src/aipass/backup/apps/handlers/diff/version_manager.py b/src/aipass/backup/apps/handlers/diff/version_manager.py index 08229c6b..3f561c12 100755 --- a/src/aipass/backup/apps/handlers/diff/version_manager.py +++ b/src/aipass/backup/apps/handlers/diff/version_manager.py @@ -25,6 +25,7 @@ from typing import Dict, List, Optional # 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 @@ -42,6 +43,7 @@ def get_versioned_files(backup_path: Path, file_path: str | None = None) -> Dict """ versioned_files = {} + json_handler.log_operation("version_discovery") if not backup_path.exists(): return versioned_files diff --git a/src/aipass/backup/apps/handlers/diff/vscode_integration.py b/src/aipass/backup/apps/handlers/diff/vscode_integration.py index 4bb7ba96..ae7bfef1 100755 --- a/src/aipass/backup/apps/handlers/diff/vscode_integration.py +++ b/src/aipass/backup/apps/handlers/diff/vscode_integration.py @@ -28,6 +28,7 @@ from pathlib import Path # 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 @@ -47,6 +48,7 @@ def show_file_diff(backup_path: Path, source_dir: Path, file_path: str, version1 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: diff --git a/src/aipass/backup/apps/handlers/models/backup_models.py b/src/aipass/backup/apps/handlers/models/backup_models.py index 027f087b..fad3abd3 100755 --- a/src/aipass/backup/apps/handlers/models/backup_models.py +++ b/src/aipass/backup/apps/handlers/models/backup_models.py @@ -22,6 +22,8 @@ from pathlib import Path import datetime from typing import List +from aipass.backup.apps.handlers.json import json_handler + # ============================================= # DATA MODELS # ============================================= @@ -50,6 +52,7 @@ class BackupResult: """ def __init__(self): + json_handler.log_operation("backup_result_created") # File statistics self.files_checked: int = 0 self.files_copied: int = 0 diff --git a/src/aipass/backup/apps/handlers/operations/drive_sync_client.py b/src/aipass/backup/apps/handlers/operations/drive_sync_client.py index 7811faa4..8062d7c5 100644 --- a/src/aipass/backup/apps/handlers/operations/drive_sync_client.py +++ b/src/aipass/backup/apps/handlers/operations/drive_sync_client.py @@ -32,21 +32,21 @@ from datetime import datetime from pathlib import Path from typing import Optional, Dict, Any, List, Tuple -# Google API imports +# Google API imports (optional — not installed in every environment) try: - from googleapiclient.discovery import build - from googleapiclient.http import MediaFileUpload - from google.auth.transport.requests import Request - from google.oauth2.credentials import Credentials - from google_auth_oauthlib.flow import InstalledAppFlow + from googleapiclient.discovery import build # type: ignore[import-unresolved] + from googleapiclient.http import MediaFileUpload # type: ignore[import-unresolved] + from google.auth.transport.requests import Request # type: ignore[import-unresolved] + from google.oauth2.credentials import Credentials # type: ignore[import-unresolved] + from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore[import-unresolved] GOOGLE_API_AVAILABLE = True except ImportError: GOOGLE_API_AVAILABLE = False - build = None - MediaFileUpload = None - Request = None - Credentials = None - InstalledAppFlow = None + build = None # type: ignore[assignment] + MediaFileUpload = None # type: ignore[assignment] + Request = None # type: ignore[assignment] + Credentials = None # type: ignore[assignment] + InstalledAppFlow = None # type: ignore[assignment] # JSON handler for data persistence from aipass.backup.apps.handlers.json.drive_sync_json import ( @@ -58,6 +58,7 @@ from aipass.backup.apps.handlers.json.drive_sync_json import ( save_log as _save_log_fn, log_operation as _log_operation_fn, ) +from aipass.backup.apps.handlers.json import json_handler # ============================================= # CONSTANTS @@ -158,6 +159,8 @@ class GoogleDriveSync: def authenticate(self) -> bool: """Authenticate with Google Drive using OAuth credentials""" + json_handler.log_operation("drive_authenticate_started") + if not GOOGLE_API_AVAILABLE: python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" @@ -170,7 +173,7 @@ class GoogleDriveSync: # Load existing credentials if they exist if self.creds_path.exists(): try: - creds = Credentials.from_authorized_user_file(str(self.creds_path), SCOPES) + creds = Credentials.from_authorized_user_file(str(self.creds_path), SCOPES) # type: ignore[union-attr] except Exception as e: creds = None @@ -179,7 +182,7 @@ class GoogleDriveSync: if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: try: - creds.refresh(Request()) + creds.refresh(Request()) # type: ignore[misc] except Exception as e: @@ -195,7 +198,7 @@ class GoogleDriveSync: try: - flow = InstalledAppFlow.from_client_secrets_file( + flow = InstalledAppFlow.from_client_secrets_file( # type: ignore[union-attr] str(self.client_secrets_path), SCOPES ) creds = flow.run_local_server(port=0) @@ -220,7 +223,7 @@ class GoogleDriveSync: # Build Drive service try: self.creds = creds # Store for building per-thread services - self.drive_service = build('drive', 'v3', credentials=creds) + self.drive_service = build('drive', 'v3', credentials=creds) # type: ignore[misc] # Update runtime state (ensure runtime_state exists first) if "runtime_state" not in self.data: @@ -492,7 +495,7 @@ class GoogleDriveSync: 'description': f'AIPass backup - {note}' if note else 'AIPass backup' } - media = MediaFileUpload(str(local_file), resumable=True) + media = MediaFileUpload(str(local_file), resumable=True) # type: ignore[misc] # Get file tracker info to avoid API calls when possible if backup_root and backup_root in local_file.parents: @@ -625,10 +628,10 @@ class GoogleDriveSync: Loads fresh credentials from disk to avoid sharing credential state (token refresh races) and creates a fully isolated HTTP/SSL connection. """ - creds = Credentials.from_authorized_user_file(str(self.creds_path), SCOPES) + creds = Credentials.from_authorized_user_file(str(self.creds_path), SCOPES) # type: ignore[union-attr] if creds and creds.expired and creds.refresh_token: - creds.refresh(Request()) - return build('drive', 'v3', credentials=creds) + creds.refresh(Request()) # type: ignore[misc] + return build('drive', 'v3', credentials=creds) # type: ignore[misc] @staticmethod def _is_ssl_error(exc: Exception) -> bool: @@ -646,7 +649,7 @@ class GoogleDriveSync: msg = str(exc) return any(kw in msg for kw in ssl_keywords) - def _api_call_with_retry(self, request, max_retries: int = 3): + def _api_call_with_retry(self, request: Any, max_retries: int = 3) -> Dict[str, Any]: """Execute a Google API request with exponential backoff on SSL errors.""" for attempt in range(max_retries + 1): try: @@ -660,6 +663,7 @@ class GoogleDriveSync: self._thread_local.service = self._build_thread_service() continue raise + raise RuntimeError("Retries exhausted") def _check_file_needs_upload_local(self, local_file: Path, backup_root: Path) -> bool: """Check if file needs upload using local tracker (no API calls)""" diff --git a/src/aipass/backup/apps/handlers/operations/drive_sync_ops.py b/src/aipass/backup/apps/handlers/operations/drive_sync_ops.py index 9fad43d3..5190fa7a 100644 --- a/src/aipass/backup/apps/handlers/operations/drive_sync_ops.py +++ b/src/aipass/backup/apps/handlers/operations/drive_sync_ops.py @@ -22,6 +22,7 @@ from aipass.backup.apps.handlers.json.drive_sync_json import ( save_data, log_operation, ) +from aipass.backup.apps.handlers.json import json_handler # JSON file paths (resolved relative to backup root) _BACKUP_ROOT = Path(__file__).resolve().parents[3] # src/aipass/backup/ @@ -38,6 +39,8 @@ def get_status() -> Dict[str, Any]: Returns: Dict with module status fields """ + json_handler.log_operation("drive_sync_status_checked") + data = load_data(_DATA_FILE) config = load_config(_CONFIG_FILE) diff --git a/src/aipass/backup/apps/handlers/operations/file_cleanup.py b/src/aipass/backup/apps/handlers/operations/file_cleanup.py index c9c0128e..82a6737b 100644 --- a/src/aipass/backup/apps/handlers/operations/file_cleanup.py +++ b/src/aipass/backup/apps/handlers/operations/file_cleanup.py @@ -24,6 +24,7 @@ from typing import Callable from aipass.backup.apps.handlers.utils.system_utils import temporarily_writable, safe_print from aipass.backup.apps.handlers.models.backup_models import BackupResult +from aipass.backup.apps.handlers.json import json_handler # ============================================= # FILE CLEANUP OPERATIONS @@ -40,6 +41,8 @@ def cleanup_deleted_files(backup_path: Path, source_dir: Path, should_ignore: Ca result: BackupResult to track deletions and errors dry_run: If True, only show what would be deleted without actually deleting """ + json_handler.log_operation("cleanup_executed") + import shutil import stat diff --git a/src/aipass/backup/apps/handlers/operations/file_operations.py b/src/aipass/backup/apps/handlers/operations/file_operations.py index 7b2168fe..a0493818 100755 --- a/src/aipass/backup/apps/handlers/operations/file_operations.py +++ b/src/aipass/backup/apps/handlers/operations/file_operations.py @@ -32,6 +32,7 @@ from pathlib import Path from aipass.backup.apps.handlers.utils.system_utils import temporarily_writable, safe_print from aipass.backup.apps.handlers.diff.diff_generator import should_create_diff, generate_diff_content from aipass.backup.apps.handlers.models.backup_models import BackupResult +from aipass.backup.apps.handlers.json import json_handler # ============================================= # SNAPSHOT MODE OPERATIONS @@ -77,6 +78,8 @@ def copy_file_with_structure(source_file: Path, target_file: Path, backup_path: safe_print(f" {error_msg}") return False + json_handler.log_operation("file_copied") + # Track if this is a new file for appropriate messaging is_new_file = not target_file.exists() diff --git a/src/aipass/backup/apps/handlers/operations/file_scanner.py b/src/aipass/backup/apps/handlers/operations/file_scanner.py index be7bb34f..408ab839 100644 --- a/src/aipass/backup/apps/handlers/operations/file_scanner.py +++ b/src/aipass/backup/apps/handlers/operations/file_scanner.py @@ -20,6 +20,8 @@ import os from pathlib import Path from typing import Callable, List, Optional +from aipass.backup.apps.handlers.json import json_handler + # ============================================= # FILE SCANNING OPERATIONS # ============================================= @@ -41,6 +43,8 @@ def scan_files(source_dir: Path, should_ignore: Callable, show_progress: bool = - file_list: List of Path objects for files to backup - skipped_items: Dict with 'directories', 'files', and 'too_large' sets """ + json_handler.log_operation("scan_started") + files_to_backup = [] skipped_items = {"directories": set(), "files": set(), "too_large": set()} max_bytes = max_file_size_mb * 1024 * 1024 if max_file_size_mb > 0 else 0 diff --git a/src/aipass/backup/apps/handlers/operations/integration_ops.py b/src/aipass/backup/apps/handlers/operations/integration_ops.py index 00dad4f5..cb9dd6aa 100644 --- a/src/aipass/backup/apps/handlers/operations/integration_ops.py +++ b/src/aipass/backup/apps/handlers/operations/integration_ops.py @@ -21,6 +21,8 @@ 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, @@ -43,6 +45,8 @@ def sync_to_drive( 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 diff --git a/src/aipass/backup/apps/handlers/operations/path_builder.py b/src/aipass/backup/apps/handlers/operations/path_builder.py index 36113c23..29630757 100644 --- a/src/aipass/backup/apps/handlers/operations/path_builder.py +++ b/src/aipass/backup/apps/handlers/operations/path_builder.py @@ -19,6 +19,8 @@ Constructs destination paths based on backup mode and file structure. import hashlib from pathlib import Path +from aipass.backup.apps.handlers.json import json_handler + # ============================================= # PATH CONSTRUCTION OPERATIONS # ============================================= @@ -35,6 +37,8 @@ def build_backup_path(source_file: Path, source_dir: Path, backup_path: Path, mo Returns: Full backup file path """ + json_handler.log_operation("path_built") + rel_path = source_file.relative_to(source_dir) if mode == 'versioned': diff --git a/src/aipass/backup/apps/handlers/operations/sync_test_ops.py b/src/aipass/backup/apps/handlers/operations/sync_test_ops.py index 09b86810..56a2b604 100644 --- a/src/aipass/backup/apps/handlers/operations/sync_test_ops.py +++ b/src/aipass/backup/apps/handlers/operations/sync_test_ops.py @@ -19,6 +19,7 @@ from pathlib import Path from datetime import datetime from aipass.prax import logger +from aipass.backup.apps.handlers.json import json_handler def create_sync_test_files(backup_root: Path) -> dict: @@ -34,6 +35,8 @@ def create_sync_test_files(backup_root: Path) -> dict: - file_count (int): Number of test files created - error (str|None): Error message if failed """ + json_handler.log_operation("sync_test_created") + try: test_dir = backup_root / "backups" / "_sync_test" test_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/aipass/backup/apps/handlers/reporting/report_formatter.py b/src/aipass/backup/apps/handlers/reporting/report_formatter.py index 71336427..ed4da47e 100644 --- a/src/aipass/backup/apps/handlers/reporting/report_formatter.py +++ b/src/aipass/backup/apps/handlers/reporting/report_formatter.py @@ -21,6 +21,7 @@ from pathlib import Path from aipass.prax import logger from aipass.backup.apps.handlers.models.backup_models import BackupResult +from aipass.backup.apps.handlers.json import json_handler # logger imported from aipass.prax @@ -45,6 +46,7 @@ def display_backup_results(result: BackupResult, mode_config: dict, backup_path: filter_tracked_items_func: Function to filter tracked items dry_run: If True, show "would be" language in statistics """ + json_handler.log_operation("report_formatted") duration = datetime.datetime.now() - result.start_time # Determine overall result status diff --git a/src/aipass/backup/apps/handlers/utils/backup_timestamps.py b/src/aipass/backup/apps/handlers/utils/backup_timestamps.py index 39d5b63a..d16db914 100644 --- a/src/aipass/backup/apps/handlers/utils/backup_timestamps.py +++ b/src/aipass/backup/apps/handlers/utils/backup_timestamps.py @@ -17,6 +17,8 @@ import json from pathlib import Path 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" @@ -44,6 +46,8 @@ def update_timestamp(mode: str) -> None: Args: mode: One of 'snapshot', 'versioned', 'drive_sync' """ + json_handler.log_operation("timestamp_updated") + data = {} if TIMESTAMPS_FILE.exists(): try: diff --git a/src/aipass/backup/apps/handlers/utils/reauth_handler.py b/src/aipass/backup/apps/handlers/utils/reauth_handler.py index f5ab09cc..fe9ee0d9 100644 --- a/src/aipass/backup/apps/handlers/utils/reauth_handler.py +++ b/src/aipass/backup/apps/handlers/utils/reauth_handler.py @@ -15,6 +15,8 @@ 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' @@ -29,13 +31,15 @@ def reauth(client_secrets_path: Path) -> bool: bool: True if authentication succeeded, False otherwise """ try: - from google_auth_oauthlib.flow import InstalledAppFlow - from google.oauth2.credentials import Credentials - from google.auth.transport.requests import Request - from googleapiclient.discovery import build + 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: diff --git a/src/aipass/backup/apps/handlers/utils/system_utils.py b/src/aipass/backup/apps/handlers/utils/system_utils.py index 3c4296b5..35945ead 100755 --- a/src/aipass/backup/apps/handlers/utils/system_utils.py +++ b/src/aipass/backup/apps/handlers/utils/system_utils.py @@ -30,6 +30,8 @@ from aipass.prax import logger from pathlib import Path from contextlib import contextmanager +from aipass.backup.apps.handlers.json import json_handler + # logger imported from aipass.prax # ============================================= @@ -121,6 +123,8 @@ def ensure_backup_directory(backup_dest: Path, backup_path: Path, is_dynamic: bo Returns: Tuple of (success: bool, error_message: str or None) """ + json_handler.log_operation("backup_directory_ensured") + try: # Check if backup_dest exists and might be read-only if backup_dest.exists(): diff --git a/src/aipass/backup/apps/modules/backup_core.py b/src/aipass/backup/apps/modules/backup_core.py index a89f1d77..050b5183 100755 --- a/src/aipass/backup/apps/modules/backup_core.py +++ b/src/aipass/backup/apps/modules/backup_core.py @@ -53,7 +53,7 @@ from aipass.backup.apps.handlers.config.config_handler import ( from aipass.backup.apps.handlers.models.backup_models import BackupResult from aipass.backup.apps.handlers.operations.file_operations import copy_file_with_structure, copy_versioned_file from aipass.backup.apps.handlers.utils.system_utils import safe_print -from aipass.backup.apps.handlers.json.json_handler import log_operation, ensure_module_jsons +from aipass.backup.apps.handlers.json import json_handler from aipass.backup.apps.handlers.json.changelog_handler import ( load_changelog as load_changelog_file, save_changelog_entry as save_changelog_entry_file, @@ -145,6 +145,10 @@ def handle_command(args) -> bool: Returns: bool: True if command was handled, False if not a backup command """ + if not args: + print_introspection() + return True + # Check if args has backup-related command if not hasattr(args, 'command'): return False @@ -266,7 +270,7 @@ class BackupEngine: self.backup_path = self.backup_dest / self.backup_folder_name # Initialize JSON system (handler creates directory if needed) - ensure_module_jsons("backup_core") + json_handler.ensure_module_jsons("backup_core") # JSON files (mode-specific) - backup-specific tracking files self.backup_info_file = JSON_DIR / f"{mode}_backup.json" @@ -497,7 +501,7 @@ class BackupEngine: # Update JSON system execution_time = int((datetime.datetime.now() - result.start_time).total_seconds() * 1000) if result.success and result.errors == 0: - log_operation( + json_handler.log_operation( "backup", { "mode": self.mode, @@ -524,7 +528,7 @@ class BackupEngine: logger.info("[backup_core] Drive sync skipped (not implemented)") logger.info("[backup_core] Read-only protection skipped (not implemented)") else: - log_operation( + json_handler.log_operation( "backup", { "mode": self.mode, diff --git a/src/aipass/backup/apps/modules/google_drive_sync.py b/src/aipass/backup/apps/modules/google_drive_sync.py index 677e2195..45781e30 100644 --- a/src/aipass/backup/apps/modules/google_drive_sync.py +++ b/src/aipass/backup/apps/modules/google_drive_sync.py @@ -23,6 +23,7 @@ from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, Ti from aipass.cli.apps.modules import console from aipass.cli.apps.modules.display import error, warning from aipass.prax import logger +from aipass.backup.apps.handlers.json import json_handler _BACKUP_ROOT = Path(__file__).resolve().parents[2] # src/aipass/backup/ @@ -62,10 +63,14 @@ except ImportError: def _load_config(): """Load config using module JSON paths.""" + if _load_config_fn is None: + return {} return _load_config_fn(CONFIG_FILE) def _load_data(): """Load data using module JSON paths.""" + if _load_data_fn is None: + return {} return _load_data_fn(DATA_FILE) @@ -93,6 +98,9 @@ from aipass.backup.apps.handlers.operations.sync_test_ops import ( def _show_file_tracker_stats() -> bool: """Display file tracker statistics.""" try: + if get_file_tracker_stats is None: + error("Drive sync dependencies not available") + return False stats = get_file_tracker_stats() console.print(f"File Tracker Statistics:") console.print(f" - Total tracked files: {stats['total']}") @@ -115,6 +123,9 @@ def _clear_file_tracker() -> bool: try: data = _load_data() tracker_count = len(data.get("runtime_state", {}).get("file_tracker", {})) + if _clear_file_tracker_handler is None: + error("Drive sync dependencies not available") + return False success = _clear_file_tracker_handler() if success: console.print(f"Cleared {tracker_count} entries from file tracker") @@ -131,10 +142,16 @@ def _clear_file_tracker() -> bool: def _test_drive_sync() -> bool: """Test Drive integration.""" try: + if GoogleDriveSync is None: + error("Drive sync dependencies not available") + return False sync = GoogleDriveSync() if not sync.authenticate(): return False console.print("Testing folder creation...") + if _test_drive_connection is None: + error("Drive sync dependencies not available") + return False result = _test_drive_connection(sync) if result: folder_id = sync.get_or_create_backup_folder() @@ -162,6 +179,10 @@ def _run_sync_test() -> bool: console.print(f" Created {setup['file_count']} test files in {test_dir}") # Run sync + if GoogleDriveSync is None: + error("Drive sync dependencies not available") + cleanup_sync_test_dir(test_dir) + return False sync = GoogleDriveSync() if not sync.authenticate(): error("Auth failed") @@ -239,6 +260,10 @@ def handle_command(args) -> bool: Returns: bool: True if command was handled, False if not a drive sync command """ + if not args: + print_introspection() + return True + if not hasattr(args, 'command'): return False @@ -286,6 +311,9 @@ def handle_command(args) -> bool: note = getattr(args, 'note', 'Manual sync') or 'Manual sync' force = getattr(args, 'force', False) + if GoogleDriveSync is None: + error("Drive sync dependencies not available") + return False sync = GoogleDriveSync() if not sync.authenticate(): error("FAILED: Could not authenticate with Google Drive") @@ -392,6 +420,7 @@ def handle_command(args) -> bool: elif command == 'drive-stats': return _show_file_tracker_stats() + json_handler.log_operation("drive_command", {"command": command}) return False # ============================================= @@ -493,6 +522,9 @@ EXAMPLES: error(f"Backup directory not found: {backup_path}") sys.exit(1) + if GoogleDriveSync is None: + error("Drive sync dependencies not available") + sys.exit(1) sync = GoogleDriveSync() if not sync.authenticate(): error("Failed to authenticate with Google Drive") diff --git a/src/aipass/backup/apps/modules/integrations.py b/src/aipass/backup/apps/modules/integrations.py index 62861dc0..461c6e0e 100755 --- a/src/aipass/backup/apps/modules/integrations.py +++ b/src/aipass/backup/apps/modules/integrations.py @@ -34,6 +34,7 @@ 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): @@ -84,9 +85,15 @@ def handle_command(args) -> bool | None: - 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 diff --git a/src/aipass/backup/apps/modules/reauth_drive.py b/src/aipass/backup/apps/modules/reauth_drive.py index 02f86e71..e25a5dcf 100644 --- a/src/aipass/backup/apps/modules/reauth_drive.py +++ b/src/aipass/backup/apps/modules/reauth_drive.py @@ -20,6 +20,7 @@ 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 @@ -37,6 +38,10 @@ def handle_command(args) -> bool: Returns: bool: True if command handled, False otherwise """ + if not args: + print_introspection() + return True + if not hasattr(args, 'command'): return False @@ -46,6 +51,7 @@ def handle_command(args) -> bool: return True if command == 'reauth': + json_handler.log_operation("reauth_command") return _execute_reauth() return False