Merge pull request #81 from AIOSAI/feat/backup-seedgo-compliance-readme-and-handler-upd

feat(backup): seedgo compliance — README and handler updates
This commit is contained in:
AIPass
2026-03-17 21:33:43 -07:00
committed by GitHub
23 changed files with 133 additions and 29 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
**Module:** `aipass.backup` **Module:** `aipass.backup`
**Created:** 2026-03-07 **Created:** 2026-03-07
**Citizen Class:** builder **Citizen Class:** builder
**Last Updated:** 2026-03-08 **Last Updated:** 2026-03-17
--- ---
@@ -25,6 +25,7 @@ from pathlib import Path
from typing import Dict, Set, List, Optional from typing import Dict, Set, List, Optional
# Re-export from ignore_patterns for backwards compatibility # 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 ( from aipass.backup.apps.handlers.config.ignore_patterns import (
GLOBAL_IGNORE_PATTERNS, IGNORE_EXCEPTIONS, should_ignore, GLOBAL_IGNORE_PATTERNS, IGNORE_EXCEPTIONS, should_ignore,
filter_tracked_items, get_ignore_patterns, get_cli_tracking_patterns, filter_tracked_items, get_ignore_patterns, get_cli_tracking_patterns,
@@ -82,6 +83,7 @@ def get_backup_destination(system_name: str) -> str:
Returns: Returns:
Path to backup destination, or base directory if not found 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) return BACKUP_DESTINATIONS.get(system_name, BASE_BACKUP_DIR)
# ============================================= # =============================================
@@ -25,6 +25,7 @@ from pathlib import Path
from typing import Dict, Set, List, Optional from typing import Dict, Set, List, Optional
from aipass.prax import logger from aipass.prax import logger
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# PATTERN LOADING # PATTERN LOADING
@@ -53,6 +54,7 @@ def load_patterns() -> dict:
with open(_PATTERNS_JSON, "r", encoding="utf-8") as f: with open(_PATTERNS_JSON, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
json_handler.log_operation("patterns_loaded")
logger.info(f"[ignore_patterns] Loaded patterns from {_PATTERNS_JSON}") logger.info(f"[ignore_patterns] Loaded patterns from {_PATTERNS_JSON}")
return data return data
@@ -24,6 +24,7 @@ from pathlib import Path
# Import from handlers # Import from handlers
from aipass.backup.apps.handlers.utils.system_utils import safe_print 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.config.config_handler import DIFF_IGNORE_PATTERNS, DIFF_INCLUDE_PATTERNS
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# DIFF GENERATION # DIFF GENERATION
@@ -101,6 +102,7 @@ def generate_diff_content(old_file: Path, new_file: Path) -> str:
lineterm='' lineterm=''
) )
json_handler.log_operation("diff_generated")
return '\n'.join(diff_lines) return '\n'.join(diff_lines)
except Exception as e: except Exception as e:
@@ -25,6 +25,7 @@ from typing import Dict, List, Optional
# Import from handlers # Import from handlers
from aipass.backup.apps.handlers.utils.system_utils import safe_print from aipass.backup.apps.handlers.utils.system_utils import safe_print
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# VERSION FILE DISCOVERY # VERSION FILE DISCOVERY
@@ -42,6 +43,7 @@ def get_versioned_files(backup_path: Path, file_path: str | None = None) -> Dict
""" """
versioned_files = {} versioned_files = {}
json_handler.log_operation("version_discovery")
if not backup_path.exists(): if not backup_path.exists():
return versioned_files return versioned_files
@@ -28,6 +28,7 @@ from pathlib import Path
# Import from handlers # Import from handlers
from aipass.backup.apps.handlers.utils.system_utils import safe_print 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.diff.version_manager import get_versioned_files
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# VS CODE INTEGRATION # 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 True if diff was opened successfully, False otherwise
""" """
try: try:
json_handler.log_operation("vscode_diff_opened")
versioned_files = get_versioned_files(backup_path, file_path) versioned_files = get_versioned_files(backup_path, file_path)
if not versioned_files: if not versioned_files:
@@ -22,6 +22,8 @@ from pathlib import Path
import datetime import datetime
from typing import List from typing import List
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# DATA MODELS # DATA MODELS
# ============================================= # =============================================
@@ -50,6 +52,7 @@ class BackupResult:
""" """
def __init__(self): def __init__(self):
json_handler.log_operation("backup_result_created")
# File statistics # File statistics
self.files_checked: int = 0 self.files_checked: int = 0
self.files_copied: int = 0 self.files_copied: int = 0
@@ -32,21 +32,21 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple from typing import Optional, Dict, Any, List, Tuple
# Google API imports # Google API imports (optional — not installed in every environment)
try: try:
from googleapiclient.discovery import build from googleapiclient.discovery import build # type: ignore[import-unresolved]
from googleapiclient.http import MediaFileUpload from googleapiclient.http import MediaFileUpload # type: ignore[import-unresolved]
from google.auth.transport.requests import Request from google.auth.transport.requests import Request # type: ignore[import-unresolved]
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials # type: ignore[import-unresolved]
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore[import-unresolved]
GOOGLE_API_AVAILABLE = True GOOGLE_API_AVAILABLE = True
except ImportError: except ImportError:
GOOGLE_API_AVAILABLE = False GOOGLE_API_AVAILABLE = False
build = None build = None # type: ignore[assignment]
MediaFileUpload = None MediaFileUpload = None # type: ignore[assignment]
Request = None Request = None # type: ignore[assignment]
Credentials = None Credentials = None # type: ignore[assignment]
InstalledAppFlow = None InstalledAppFlow = None # type: ignore[assignment]
# JSON handler for data persistence # JSON handler for data persistence
from aipass.backup.apps.handlers.json.drive_sync_json import ( 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, save_log as _save_log_fn,
log_operation as _log_operation_fn, log_operation as _log_operation_fn,
) )
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# CONSTANTS # CONSTANTS
@@ -158,6 +159,8 @@ class GoogleDriveSync:
def authenticate(self) -> bool: def authenticate(self) -> bool:
"""Authenticate with Google Drive using OAuth credentials""" """Authenticate with Google Drive using OAuth credentials"""
json_handler.log_operation("drive_authenticate_started")
if not GOOGLE_API_AVAILABLE: if not GOOGLE_API_AVAILABLE:
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" 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 # Load existing credentials if they exist
if self.creds_path.exists(): if self.creds_path.exists():
try: 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: except Exception as e:
creds = None creds = None
@@ -179,7 +182,7 @@ class GoogleDriveSync:
if not creds or not creds.valid: if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token: if creds and creds.expired and creds.refresh_token:
try: try:
creds.refresh(Request()) creds.refresh(Request()) # type: ignore[misc]
except Exception as e: except Exception as e:
@@ -195,7 +198,7 @@ class GoogleDriveSync:
try: try:
flow = InstalledAppFlow.from_client_secrets_file( flow = InstalledAppFlow.from_client_secrets_file( # type: ignore[union-attr]
str(self.client_secrets_path), SCOPES str(self.client_secrets_path), SCOPES
) )
creds = flow.run_local_server(port=0) creds = flow.run_local_server(port=0)
@@ -220,7 +223,7 @@ class GoogleDriveSync:
# Build Drive service # Build Drive service
try: try:
self.creds = creds # Store for building per-thread services 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) # Update runtime state (ensure runtime_state exists first)
if "runtime_state" not in self.data: if "runtime_state" not in self.data:
@@ -492,7 +495,7 @@ class GoogleDriveSync:
'description': f'AIPass backup - {note}' if note else 'AIPass backup' '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 # Get file tracker info to avoid API calls when possible
if backup_root and backup_root in local_file.parents: 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 Loads fresh credentials from disk to avoid sharing credential state
(token refresh races) and creates a fully isolated HTTP/SSL connection. (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: if creds and creds.expired and creds.refresh_token:
creds.refresh(Request()) creds.refresh(Request()) # type: ignore[misc]
return build('drive', 'v3', credentials=creds) return build('drive', 'v3', credentials=creds) # type: ignore[misc]
@staticmethod @staticmethod
def _is_ssl_error(exc: Exception) -> bool: def _is_ssl_error(exc: Exception) -> bool:
@@ -646,7 +649,7 @@ class GoogleDriveSync:
msg = str(exc) msg = str(exc)
return any(kw in msg for kw in ssl_keywords) 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.""" """Execute a Google API request with exponential backoff on SSL errors."""
for attempt in range(max_retries + 1): for attempt in range(max_retries + 1):
try: try:
@@ -660,6 +663,7 @@ class GoogleDriveSync:
self._thread_local.service = self._build_thread_service() self._thread_local.service = self._build_thread_service()
continue continue
raise raise
raise RuntimeError("Retries exhausted")
def _check_file_needs_upload_local(self, local_file: Path, backup_root: Path) -> bool: 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)""" """Check if file needs upload using local tracker (no API calls)"""
@@ -22,6 +22,7 @@ from aipass.backup.apps.handlers.json.drive_sync_json import (
save_data, save_data,
log_operation, log_operation,
) )
from aipass.backup.apps.handlers.json import json_handler
# JSON file paths (resolved relative to backup root) # JSON file paths (resolved relative to backup root)
_BACKUP_ROOT = Path(__file__).resolve().parents[3] # src/aipass/backup/ _BACKUP_ROOT = Path(__file__).resolve().parents[3] # src/aipass/backup/
@@ -38,6 +39,8 @@ def get_status() -> Dict[str, Any]:
Returns: Returns:
Dict with module status fields Dict with module status fields
""" """
json_handler.log_operation("drive_sync_status_checked")
data = load_data(_DATA_FILE) data = load_data(_DATA_FILE)
config = load_config(_CONFIG_FILE) config = load_config(_CONFIG_FILE)
@@ -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.utils.system_utils import temporarily_writable, safe_print
from aipass.backup.apps.handlers.models.backup_models import BackupResult from aipass.backup.apps.handlers.models.backup_models import BackupResult
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# FILE CLEANUP OPERATIONS # 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 result: BackupResult to track deletions and errors
dry_run: If True, only show what would be deleted without actually deleting dry_run: If True, only show what would be deleted without actually deleting
""" """
json_handler.log_operation("cleanup_executed")
import shutil import shutil
import stat import stat
@@ -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.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.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.models.backup_models import BackupResult
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# SNAPSHOT MODE OPERATIONS # 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}") safe_print(f" {error_msg}")
return False return False
json_handler.log_operation("file_copied")
# Track if this is a new file for appropriate messaging # Track if this is a new file for appropriate messaging
is_new_file = not target_file.exists() is_new_file = not target_file.exists()
@@ -20,6 +20,8 @@ import os
from pathlib import Path from pathlib import Path
from typing import Callable, List, Optional from typing import Callable, List, Optional
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# FILE SCANNING OPERATIONS # 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 - file_list: List of Path objects for files to backup
- skipped_items: Dict with 'directories', 'files', and 'too_large' sets - skipped_items: Dict with 'directories', 'files', and 'too_large' sets
""" """
json_handler.log_operation("scan_started")
files_to_backup = [] files_to_backup = []
skipped_items = {"directories": set(), "files": set(), "too_large": set()} 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 max_bytes = max_file_size_mb * 1024 * 1024 if max_file_size_mb > 0 else 0
@@ -21,6 +21,8 @@ import stat
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from aipass.backup.apps.handlers.json import json_handler
def sync_to_drive( def sync_to_drive(
backup_path: Path, backup_path: Path,
@@ -43,6 +45,8 @@ def sync_to_drive(
Returns: Returns:
bool: True if sync succeeded, False otherwise 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: if not drive_sync_available or drive_sync_module is None:
return False return False
@@ -19,6 +19,8 @@ Constructs destination paths based on backup mode and file structure.
import hashlib import hashlib
from pathlib import Path from pathlib import Path
from aipass.backup.apps.handlers.json import json_handler
# ============================================= # =============================================
# PATH CONSTRUCTION OPERATIONS # PATH CONSTRUCTION OPERATIONS
# ============================================= # =============================================
@@ -35,6 +37,8 @@ def build_backup_path(source_file: Path, source_dir: Path, backup_path: Path, mo
Returns: Returns:
Full backup file path Full backup file path
""" """
json_handler.log_operation("path_built")
rel_path = source_file.relative_to(source_dir) rel_path = source_file.relative_to(source_dir)
if mode == 'versioned': if mode == 'versioned':
@@ -19,6 +19,7 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from aipass.prax import logger from aipass.prax import logger
from aipass.backup.apps.handlers.json import json_handler
def create_sync_test_files(backup_root: Path) -> dict: 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 - file_count (int): Number of test files created
- error (str|None): Error message if failed - error (str|None): Error message if failed
""" """
json_handler.log_operation("sync_test_created")
try: try:
test_dir = backup_root / "backups" / "_sync_test" test_dir = backup_root / "backups" / "_sync_test"
test_dir.mkdir(parents=True, exist_ok=True) test_dir.mkdir(parents=True, exist_ok=True)
@@ -21,6 +21,7 @@ from pathlib import Path
from aipass.prax import logger from aipass.prax import logger
from aipass.backup.apps.handlers.models.backup_models import BackupResult from aipass.backup.apps.handlers.models.backup_models import BackupResult
from aipass.backup.apps.handlers.json import json_handler
# logger imported from aipass.prax # 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 filter_tracked_items_func: Function to filter tracked items
dry_run: If True, show "would be" language in statistics dry_run: If True, show "would be" language in statistics
""" """
json_handler.log_operation("report_formatted")
duration = datetime.datetime.now() - result.start_time duration = datetime.datetime.now() - result.start_time
# Determine overall result status # Determine overall result status
@@ -17,6 +17,8 @@ import json
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from aipass.backup.apps.handlers.json import json_handler
_BACKUP_ROOT = Path(__file__).resolve().parents[3] # src/aipass/backup/ _BACKUP_ROOT = Path(__file__).resolve().parents[3] # src/aipass/backup/
TIMESTAMPS_FILE = _BACKUP_ROOT / "backup_data" / "backup_timestamps.json" TIMESTAMPS_FILE = _BACKUP_ROOT / "backup_data" / "backup_timestamps.json"
@@ -44,6 +46,8 @@ def update_timestamp(mode: str) -> None:
Args: Args:
mode: One of 'snapshot', 'versioned', 'drive_sync' mode: One of 'snapshot', 'versioned', 'drive_sync'
""" """
json_handler.log_operation("timestamp_updated")
data = {} data = {}
if TIMESTAMPS_FILE.exists(): if TIMESTAMPS_FILE.exists():
try: try:
@@ -15,6 +15,8 @@ Called by the reauth_drive module orchestrator.
from pathlib import Path from pathlib import Path
from aipass.backup.apps.handlers.json import json_handler
SCOPES = ['https://www.googleapis.com/auth/drive.file'] SCOPES = ['https://www.googleapis.com/auth/drive.file']
CREDS_PATH = Path.home() / '.aipass' / 'drive_creds.json' 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 bool: True if authentication succeeded, False otherwise
""" """
try: try:
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore[import-unresolved]
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials # type: ignore[import-unresolved]
from google.auth.transport.requests import Request from google.auth.transport.requests import Request # type: ignore[import-unresolved]
from googleapiclient.discovery import build from googleapiclient.discovery import build # type: ignore[import-unresolved]
except ImportError as e: except ImportError as e:
return False return False
json_handler.log_operation("reauth_initiated")
# Step 1: Try refreshing existing token first # Step 1: Try refreshing existing token first
if CREDS_PATH.exists(): if CREDS_PATH.exists():
try: try:
@@ -30,6 +30,8 @@ from aipass.prax import logger
from pathlib import Path from pathlib import Path
from contextlib import contextmanager from contextlib import contextmanager
from aipass.backup.apps.handlers.json import json_handler
# logger imported from aipass.prax # logger imported from aipass.prax
# ============================================= # =============================================
@@ -121,6 +123,8 @@ def ensure_backup_directory(backup_dest: Path, backup_path: Path, is_dynamic: bo
Returns: Returns:
Tuple of (success: bool, error_message: str or None) Tuple of (success: bool, error_message: str or None)
""" """
json_handler.log_operation("backup_directory_ensured")
try: try:
# Check if backup_dest exists and might be read-only # Check if backup_dest exists and might be read-only
if backup_dest.exists(): if backup_dest.exists():
@@ -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.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.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.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 ( from aipass.backup.apps.handlers.json.changelog_handler import (
load_changelog as load_changelog_file, load_changelog as load_changelog_file,
save_changelog_entry as save_changelog_entry_file, save_changelog_entry as save_changelog_entry_file,
@@ -145,6 +145,10 @@ def handle_command(args) -> bool:
Returns: Returns:
bool: True if command was handled, False if not a backup command 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 # Check if args has backup-related command
if not hasattr(args, 'command'): if not hasattr(args, 'command'):
return False return False
@@ -266,7 +270,7 @@ class BackupEngine:
self.backup_path = self.backup_dest / self.backup_folder_name self.backup_path = self.backup_dest / self.backup_folder_name
# Initialize JSON system (handler creates directory if needed) # 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 # JSON files (mode-specific) - backup-specific tracking files
self.backup_info_file = JSON_DIR / f"{mode}_backup.json" self.backup_info_file = JSON_DIR / f"{mode}_backup.json"
@@ -497,7 +501,7 @@ class BackupEngine:
# Update JSON system # Update JSON system
execution_time = int((datetime.datetime.now() - result.start_time).total_seconds() * 1000) execution_time = int((datetime.datetime.now() - result.start_time).total_seconds() * 1000)
if result.success and result.errors == 0: if result.success and result.errors == 0:
log_operation( json_handler.log_operation(
"backup", "backup",
{ {
"mode": self.mode, "mode": self.mode,
@@ -524,7 +528,7 @@ class BackupEngine:
logger.info("[backup_core] Drive sync skipped (not implemented)") logger.info("[backup_core] Drive sync skipped (not implemented)")
logger.info("[backup_core] Read-only protection skipped (not implemented)") logger.info("[backup_core] Read-only protection skipped (not implemented)")
else: else:
log_operation( json_handler.log_operation(
"backup", "backup",
{ {
"mode": self.mode, "mode": self.mode,
@@ -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 import console
from aipass.cli.apps.modules.display import error, warning from aipass.cli.apps.modules.display import error, warning
from aipass.prax import logger from aipass.prax import logger
from aipass.backup.apps.handlers.json import json_handler
_BACKUP_ROOT = Path(__file__).resolve().parents[2] # src/aipass/backup/ _BACKUP_ROOT = Path(__file__).resolve().parents[2] # src/aipass/backup/
@@ -62,10 +63,14 @@ except ImportError:
def _load_config(): def _load_config():
"""Load config using module JSON paths.""" """Load config using module JSON paths."""
if _load_config_fn is None:
return {}
return _load_config_fn(CONFIG_FILE) return _load_config_fn(CONFIG_FILE)
def _load_data(): def _load_data():
"""Load data using module JSON paths.""" """Load data using module JSON paths."""
if _load_data_fn is None:
return {}
return _load_data_fn(DATA_FILE) 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: def _show_file_tracker_stats() -> bool:
"""Display file tracker statistics.""" """Display file tracker statistics."""
try: try:
if get_file_tracker_stats is None:
error("Drive sync dependencies not available")
return False
stats = get_file_tracker_stats() stats = get_file_tracker_stats()
console.print(f"File Tracker Statistics:") console.print(f"File Tracker Statistics:")
console.print(f" - Total tracked files: {stats['total']}") console.print(f" - Total tracked files: {stats['total']}")
@@ -115,6 +123,9 @@ def _clear_file_tracker() -> bool:
try: try:
data = _load_data() data = _load_data()
tracker_count = len(data.get("runtime_state", {}).get("file_tracker", {})) 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() success = _clear_file_tracker_handler()
if success: if success:
console.print(f"Cleared {tracker_count} entries from file tracker") console.print(f"Cleared {tracker_count} entries from file tracker")
@@ -131,10 +142,16 @@ def _clear_file_tracker() -> bool:
def _test_drive_sync() -> bool: def _test_drive_sync() -> bool:
"""Test Drive integration.""" """Test Drive integration."""
try: try:
if GoogleDriveSync is None:
error("Drive sync dependencies not available")
return False
sync = GoogleDriveSync() sync = GoogleDriveSync()
if not sync.authenticate(): if not sync.authenticate():
return False return False
console.print("Testing folder creation...") console.print("Testing folder creation...")
if _test_drive_connection is None:
error("Drive sync dependencies not available")
return False
result = _test_drive_connection(sync) result = _test_drive_connection(sync)
if result: if result:
folder_id = sync.get_or_create_backup_folder() 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}") console.print(f" Created {setup['file_count']} test files in {test_dir}")
# Run sync # Run sync
if GoogleDriveSync is None:
error("Drive sync dependencies not available")
cleanup_sync_test_dir(test_dir)
return False
sync = GoogleDriveSync() sync = GoogleDriveSync()
if not sync.authenticate(): if not sync.authenticate():
error("Auth failed") error("Auth failed")
@@ -239,6 +260,10 @@ def handle_command(args) -> bool:
Returns: Returns:
bool: True if command was handled, False if not a drive sync command 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'): if not hasattr(args, 'command'):
return False return False
@@ -286,6 +311,9 @@ def handle_command(args) -> bool:
note = getattr(args, 'note', 'Manual sync') or 'Manual sync' note = getattr(args, 'note', 'Manual sync') or 'Manual sync'
force = getattr(args, 'force', False) force = getattr(args, 'force', False)
if GoogleDriveSync is None:
error("Drive sync dependencies not available")
return False
sync = GoogleDriveSync() sync = GoogleDriveSync()
if not sync.authenticate(): if not sync.authenticate():
error("FAILED: Could not authenticate with Google Drive") error("FAILED: Could not authenticate with Google Drive")
@@ -392,6 +420,7 @@ def handle_command(args) -> bool:
elif command == 'drive-stats': elif command == 'drive-stats':
return _show_file_tracker_stats() return _show_file_tracker_stats()
json_handler.log_operation("drive_command", {"command": command})
return False return False
# ============================================= # =============================================
@@ -493,6 +522,9 @@ EXAMPLES:
error(f"Backup directory not found: {backup_path}") error(f"Backup directory not found: {backup_path}")
sys.exit(1) sys.exit(1)
if GoogleDriveSync is None:
error("Drive sync dependencies not available")
sys.exit(1)
sync = GoogleDriveSync() sync = GoogleDriveSync()
if not sync.authenticate(): if not sync.authenticate():
error("Failed to authenticate with Google Drive") error("Failed to authenticate with Google Drive")
@@ -34,6 +34,7 @@ from pathlib import Path
from aipass.cli.apps.modules import console from aipass.cli.apps.modules import console
from aipass.prax import logger from aipass.prax import logger
from aipass.backup.apps.handlers.json import json_handler
def _header(text): def _header(text):
@@ -84,9 +85,15 @@ def handle_command(args) -> bool | None:
- sync-to-drive: Sync backup to Google Drive - sync-to-drive: Sync backup to Google Drive
- set-readonly: Protect backup with read-only permissions - set-readonly: Protect backup with read-only permissions
""" """
if not args:
print_introspection()
return True
if not hasattr(args, 'integration_command'): if not hasattr(args, 'integration_command'):
return None return None
json_handler.log_operation("integration_command")
if args.integration_command == 'sync-to-drive': if args.integration_command == 'sync-to-drive':
backup_path = Path(args.backup_path) if hasattr(args, 'backup_path') else None 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 source_dir = Path(args.source_dir) if hasattr(args, 'source_dir') else None
@@ -20,6 +20,7 @@ from pathlib import Path
from aipass.cli.apps.modules import console from aipass.cli.apps.modules import console
from aipass.cli.apps.modules.display import error from aipass.cli.apps.modules.display import error
from aipass.prax import logger from aipass.prax import logger
from aipass.backup.apps.handlers.json import json_handler
# Handler imports # Handler imports
from aipass.backup.apps.handlers.utils.reauth_handler import reauth as _run_reauth from aipass.backup.apps.handlers.utils.reauth_handler import reauth as _run_reauth
@@ -37,6 +38,10 @@ def handle_command(args) -> bool:
Returns: Returns:
bool: True if command handled, False otherwise bool: True if command handled, False otherwise
""" """
if not args:
print_introspection()
return True
if not hasattr(args, 'command'): if not hasattr(args, 'command'):
return False return False
@@ -46,6 +51,7 @@ def handle_command(args) -> bool:
return True return True
if command == 'reauth': if command == 'reauth':
json_handler.log_operation("reauth_command")
return _execute_reauth() return _execute_reauth()
return False return False