feat(backup): seedgo compliance — README and handler updates
Co-Authored-By: @backup <backup@aipass>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user