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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user