Merge pull request #105 from AIOSAI/feat/backup-fixbackup-archive-obsolete-files-timesta

feat(backup): fix(backup): archive obsolete files, timestamps consolidation, plugin cleanup
This commit is contained in:
AIPass
2026-03-21 23:12:44 -07:00
committed by GitHub
14 changed files with 11 additions and 1027 deletions
+2 -2
View File
@@ -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
@@ -1 +0,0 @@
# Extensions package - Drop-in extensions for branch functionality
@@ -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<timestamp>.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 <path> 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
@@ -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
@@ -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}"
@@ -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"]
@@ -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
@@ -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
@@ -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 <path> ...\033[0m")
safe_print(" \033[90m$ backup integration set-readonly --backup-path <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()
@@ -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)
@@ -1 +0,0 @@
# Plugins package - Pluggable components for branch capabilities
+1
View File
@@ -0,0 +1 @@
# Placeholder
@@ -1,9 +0,0 @@
# TPLAN-0001: Test TPLAN auto-discovery
Tag:
> Test template for auto-discovery verification
---
*Created: 2026-03-18*
@@ -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*