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`
**Created:** 2026-03-07
**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
# 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)
# =============================================
@@ -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
@@ -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:
@@ -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
@@ -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:
@@ -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
@@ -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)"""
@@ -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)
@@ -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
@@ -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()
@@ -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
@@ -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
@@ -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':
@@ -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)
@@ -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
@@ -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:
@@ -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:
@@ -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():
@@ -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,
@@ -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")
@@ -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
@@ -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