diff --git a/src/basic_memory/cli/commands/status.py b/src/basic_memory/cli/commands/status.py index 0fe33b93..90e9401e 100644 --- a/src/basic_memory/cli/commands/status.py +++ b/src/basic_memory/cli/commands/status.py @@ -14,14 +14,14 @@ from basic_memory.cli.app import app from basic_memory.config import config from basic_memory.db import DatabaseType from basic_memory.repository import DocumentRepository, EntityRepository -from basic_memory.services import FileSyncService -from basic_memory.services.file_sync_service import SyncReport, FileState +from basic_memory.services import FileChangeScanner +from basic_memory.services.utils import SyncReport, FileState # Create rich console console = Console() -async def get_sync_service(db_type=DatabaseType.FILESYSTEM) -> FileSyncService: +async def get_file_change_scanner(db_type=DatabaseType.FILESYSTEM) -> FileChangeScanner: """Get sync service instance.""" async with db.engine_session_factory(db_path=config.database_path, db_type=db_type) as ( engine, @@ -29,8 +29,8 @@ async def get_sync_service(db_type=DatabaseType.FILESYSTEM) -> FileSyncService: ): document_repository = DocumentRepository(session_maker) entity_repository = EntityRepository(session_maker) - sync_service = FileSyncService(document_repository, entity_repository) - return sync_service + file_change_scanner = FileChangeScanner(document_repository, entity_repository) + return file_change_scanner def add_files_to_tree(tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] = None): @@ -159,7 +159,7 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False): console.print(Panel(tree, expand=False)) -async def run_status(sync_service: FileSyncService, verbose: bool = False): +async def run_status(sync_service: FileChangeScanner, verbose: bool = False): """Check sync status of files vs database.""" # Check knowledge/ directory @@ -177,7 +177,7 @@ def status( ): """Show sync status between files and database.""" try: - sync_service = asyncio.run(get_sync_service()) + sync_service = asyncio.run(get_file_change_scanner()) asyncio.run(run_status(sync_service, verbose)) except Exception as e: logger.error(f"Error checking status: {e}") diff --git a/src/basic_memory/services/__init__.py b/src/basic_memory/services/__init__.py index 65477c53..4d070fea 100644 --- a/src/basic_memory/services/__init__.py +++ b/src/basic_memory/services/__init__.py @@ -1,7 +1,7 @@ """Services package.""" from .document_service import DocumentService -from .document_sync_service import DocumentSyncService +from .sync.document_sync_service import DocumentSyncService from .entity_service import EntityService from .file_change_scanner import FileChangeScanner from .file_service import FileService diff --git a/src/basic_memory/services/file_change_scanner.py b/src/basic_memory/services/file_change_scanner.py index 82e34ce0..af9e64fd 100644 --- a/src/basic_memory/services/file_change_scanner.py +++ b/src/basic_memory/services/file_change_scanner.py @@ -1,38 +1,16 @@ """Service for detecting changes between filesystem and database.""" -from dataclasses import dataclass, field from pathlib import Path -from typing import Set, Dict, Protocol, TypeVar, Generic, Optional +from typing import Dict, Protocol, TypeVar, Optional from loguru import logger from basic_memory.repository.document_repository import DocumentRepository from basic_memory.repository.entity_repository import EntityRepository +from basic_memory.services.utils import FileState, SyncReport from basic_memory.utils.file_utils import compute_checksum -@dataclass -class FileState: - """State of a file including path and checksum info.""" - path: str - checksum: str - moved_from: Optional[str] = None - - -@dataclass -class SyncReport: - """Report of file changes found.""" - new: Set[str] = field(default_factory=set) - modified: Set[str] = field(default_factory=set) - deleted: Set[str] = field(default_factory=set) - moved: Dict[str, FileState] = field(default_factory=dict) # new_path -> state - checksums: Dict[str, str] = field(default_factory=dict) # path -> checksum - - @property - def total_changes(self) -> int: - return len(self.new) + len(self.modified) + len(self.deleted) + len(self.moved) - - class DbRecord(Protocol): """Protocol for database records with path and checksum.""" @property diff --git a/src/basic_memory/services/sync/__init__.py b/src/basic_memory/services/sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/basic_memory/services/document_sync_service.py b/src/basic_memory/services/sync/document_sync_service.py similarity index 97% rename from src/basic_memory/services/document_sync_service.py rename to src/basic_memory/services/sync/document_sync_service.py index bb5ec492..e49c6c7a 100644 --- a/src/basic_memory/services/document_sync_service.py +++ b/src/basic_memory/services/sync/document_sync_service.py @@ -5,7 +5,8 @@ from pathlib import Path from loguru import logger from basic_memory.services.document_service import DocumentService -from basic_memory.services.file_change_scanner import FileChangeScanner, SyncReport +from basic_memory.services.file_change_scanner import FileChangeScanner +from basic_memory.services.utils import SyncReport class DocumentSyncService: diff --git a/src/basic_memory/services/sync/knowledge_sync_service.py b/src/basic_memory/services/sync/knowledge_sync_service.py new file mode 100644 index 00000000..fa96cb14 --- /dev/null +++ b/src/basic_memory/services/sync/knowledge_sync_service.py @@ -0,0 +1,59 @@ +from pathlib import Path + +from basic_memory.markdown import KnowledgeParser +from basic_memory.services import FileChangeScanner, KnowledgeService +from basic_memory.services.utils import FileState, SyncReport + + +class KnowledgeSyncService: + def __init__( + self, + scanner: FileChangeScanner, + knowledge_service: KnowledgeService, + knowledge_parser: KnowledgeParser, + ): + self.scanner = scanner + self.knowledge_service = knowledge_service + self.knowledge_parser = knowledge_parser + + async def sync_new_entity(self, directory: Path, path: str) -> None: + """Handle syncing a new entity file.""" + entity = await self.knowledge_parser.parse_file(directory / path) + await self.knowledge_service.create_entity(entity) + + async def sync_modified_entity(self, directory: Path, path: str) -> None: + """Handle syncing a modified entity file.""" + entity = await self.knowledge_parser.parse_file(directory / path) + # TODO: Update vs create, preserve relations + await self.knowledge_service.create_entity(entity) + + async def sync_moved_entity(self, directory: Path, new_path: str, state: FileState) -> None: + """Handle syncing a moved entity file.""" + new_entity = await self.knowledge_parser.parse_file(directory / new_path) + old_entity = await self.knowledge_service.get_entity_by_path_id(state.moved_from) + # TODO: Preserve relations, update paths + await self.knowledge_service.create_entity(new_entity) + + async def sync_deleted_entity(self, path: str) -> None: + """Handle syncing a deleted entity file.""" + await self.knowledge_service.delete_entity(path) + + async def sync(self, directory: Path) -> SyncReport: + """Sync knowledge files between filesystem and database.""" + # Find all changes + changes = await self.scanner.find_knowledge_changes(directory) + + # Process each type of change + for path in changes.new: + await self.sync_new_entity(directory, path) + + for path in changes.modified: + await self.sync_modified_entity(directory, path) + + for new_path, state in changes.moved.items(): + await self.sync_moved_entity(directory, new_path, state) + + for path in changes.deleted: + await self.sync_deleted_entity(path) + + return changes diff --git a/src/basic_memory/services/utils.py b/src/basic_memory/services/utils.py new file mode 100644 index 00000000..170dee53 --- /dev/null +++ b/src/basic_memory/services/utils.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass, field +from typing import Optional, Set, Dict + + +@dataclass +class FileState: + """State of a file including path and checksum info.""" + path: str + checksum: str + moved_from: Optional[str] = None + + +@dataclass +class SyncReport: + """Report of file changes found.""" + new: Set[str] = field(default_factory=set) + modified: Set[str] = field(default_factory=set) + deleted: Set[str] = field(default_factory=set) + moved: Dict[str, FileState] = field(default_factory=dict) # new_path -> state + checksums: Dict[str, str] = field(default_factory=dict) # path -> checksum + + @property + def total_changes(self) -> int: + return len(self.new) + len(self.modified) + len(self.deleted) + len(self.moved)