Addressed issues when running basic-memory on the Windows platform (#252)

Signed-off-by: Manuel Bliemel <manuel.bliemel@gmail.com>
This commit is contained in:
manuelbliemel
2025-08-25 04:12:40 +02:00
committed by GitHub
parent 7aff836c57
commit 9aa40246a8
21 changed files with 93 additions and 70 deletions
+2 -2
View File
@@ -74,7 +74,7 @@ def add_project(
) -> None:
"""Add a new project."""
# Resolve to absolute path
resolved_path = os.path.abspath(os.path.expanduser(path))
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
try:
data = {"name": name, "path": resolved_path, "set_default": set_default}
@@ -156,7 +156,7 @@ def move_project(
) -> None:
"""Move a project to a new location."""
# Resolve to absolute path
resolved_path = os.path.abspath(os.path.expanduser(new_path))
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
try:
data = {"path": resolved_path}
+4 -4
View File
@@ -46,7 +46,7 @@ class BasicMemoryConfig(BaseSettings):
projects: Dict[str, str] = Field(
default_factory=lambda: {
"main": str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
"main": Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")).as_posix()
},
description="Mapping of project names to their filesystem paths",
)
@@ -100,9 +100,9 @@ class BasicMemoryConfig(BaseSettings):
"""Ensure configuration is valid after initialization."""
# Ensure main project exists
if "main" not in self.projects: # pragma: no cover
self.projects["main"] = str(
self.projects["main"] = (
Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
)
).as_posix()
# Ensure default project is valid
if self.default_project not in self.projects: # pragma: no cover
@@ -215,7 +215,7 @@ class ConfigManager:
# Load config, modify it, and save it
config = self.load_config()
config.projects[name] = str(project_path)
config.projects[name] = project_path.as_posix()
self.save_config(config)
return ProjectConfig(name=name, home=project_path)
+1 -1
View File
@@ -41,7 +41,7 @@ def entity_model_from_markdown(
# Only update permalink if it exists in frontmatter, otherwise preserve existing
if markdown.frontmatter.permalink is not None:
model.permalink = markdown.frontmatter.permalink
model.file_path = str(file_path)
model.file_path = file_path.as_posix()
model.content_type = "text/markdown"
model.created_at = markdown.created
model.updated_at = markdown.modified
@@ -57,7 +57,7 @@ class EntityRepository(Repository[Entity]):
"""
query = (
self.select()
.where(Entity.file_path == str(file_path))
.where(Entity.file_path == Path(file_path).as_posix())
.options(*self.get_load_options())
)
return await self.find_one(query)
@@ -68,7 +68,7 @@ class EntityRepository(Repository[Entity]):
Args:
file_path: Path to the entity file (will be converted to string internally)
"""
return await self.delete_by_fields(file_path=str(file_path))
return await self.delete_by_fields(file_path=Path(file_path).as_posix())
def get_load_options(self) -> List[LoaderOption]:
"""Get SQLAlchemy loader options for eager loading relationships."""
@@ -46,7 +46,7 @@ class ProjectRepository(Repository[Project]):
Args:
path: Path to the project directory (will be converted to string internally)
"""
query = self.select().where(Project.path == str(path))
query = self.select().where(Project.path == Path(path).as_posix())
return await self.find_one(query)
async def get_default_project(self) -> Optional[Project]:
@@ -6,6 +6,7 @@ import time
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional
from pathlib import Path
from loguru import logger
from sqlalchemy import Executable, Result, text
@@ -59,8 +60,11 @@ class SearchIndexRow:
if not self.type == SearchItemType.ENTITY.value and not self.file_path:
return ""
# Normalize path separators to handle both Windows (\) and Unix (/) paths
normalized_path = Path(self.file_path).as_posix()
# Split the path by slashes
parts = self.file_path.split("/")
parts = normalized_path.split("/")
# If there's only one part (e.g., "README.md"), it's at the root
if len(parts) <= 1:
+5 -5
View File
@@ -91,7 +91,7 @@ class EntityService(BaseService[EntityModel]):
Enhanced to detect and handle character-related conflicts.
"""
file_path_str = str(file_path)
file_path_str = Path(file_path).as_posix()
# Check for potential file path conflicts before resolving permalink
conflicts = await self.detect_file_path_conflicts(file_path_str)
@@ -119,7 +119,7 @@ class EntityService(BaseService[EntityModel]):
if markdown and markdown.frontmatter.permalink:
desired_permalink = markdown.frontmatter.permalink
else:
desired_permalink = generate_permalink(file_path)
desired_permalink = generate_permalink(file_path_str)
# Make unique if needed - enhanced to handle character conflicts
permalink = desired_permalink
@@ -283,7 +283,7 @@ class EntityService(BaseService[EntityModel]):
entity = await self.update_entity_and_observations(file_path, entity_markdown)
# add relations
await self.update_entity_relations(str(file_path), entity_markdown)
await self.update_entity_relations(file_path.as_posix(), entity_markdown)
# Set final checksum to match file
entity = await self.repository.update(entity.id, {"checksum": checksum})
@@ -374,7 +374,7 @@ class EntityService(BaseService[EntityModel]):
"""
logger.debug(f"Updating entity and observations: {file_path}")
db_entity = await self.repository.get_by_file_path(str(file_path))
db_entity = await self.repository.get_by_file_path(file_path.as_posix())
# Clear observations for entity
await self.observation_repository.delete_by_fields(entity_id=db_entity.id)
@@ -498,7 +498,7 @@ class EntityService(BaseService[EntityModel]):
# Update entity and its relationships
entity = await self.update_entity_and_observations(file_path, entity_markdown)
await self.update_entity_relations(str(file_path), entity_markdown)
await self.update_entity_relations(file_path.as_posix(), entity_markdown)
# Set final checksum to match file
entity = await self.repository.update(entity.id, {"checksum": checksum})
+3 -3
View File
@@ -100,7 +100,7 @@ class ProjectService:
raise ValueError("Repository is required for add_project")
# Resolve to absolute path
resolved_path = os.path.abspath(os.path.expanduser(path))
resolved_path = Path(os.path.abspath(os.path.expanduser(path))).as_posix()
# First add to config file (this will validate the project doesn't exist)
project_config = self.config_manager.add_project(name, resolved_path)
@@ -323,7 +323,7 @@ class ProjectService:
raise ValueError("Repository is required for move_project")
# Resolve to absolute path
resolved_path = os.path.abspath(os.path.expanduser(new_path))
resolved_path = Path(os.path.abspath(os.path.expanduser(new_path))).as_posix()
# Validate project exists in config
if name not in self.config_manager.projects:
@@ -378,7 +378,7 @@ class ProjectService:
# Update path if provided
if updated_path:
resolved_path = os.path.abspath(os.path.expanduser(updated_path))
resolved_path = Path(os.path.abspath(os.path.expanduser(updated_path))).as_posix()
# Update in config
config = self.config_manager.load_config()
+1 -1
View File
@@ -619,7 +619,7 @@ class SyncService:
continue
path = Path(root) / filename
rel_path = str(path.relative_to(directory))
rel_path = path.relative_to(directory).as_posix()
checksum = await self.file_service.compute_checksum(rel_path)
result.files[rel_path] = checksum
result.checksums[checksum] = rel_path
+1 -1
View File
@@ -197,7 +197,7 @@ class WatchService:
for change, path in changes:
# convert to relative path
relative_path = str(Path(path).relative_to(directory))
relative_path = Path(path).relative_to(directory).as_posix()
# Skip .tmp files - they're temporary and shouldn't be synced
if relative_path.endswith(".tmp"):
+13 -1
View File
@@ -49,7 +49,7 @@ def generate_permalink(file_path: Union[Path, str, PathLike]) -> str:
'中文/测试文档'
"""
# Convert Path to string if needed
path_str = str(file_path)
path_str = Path(str(file_path)).as_posix()
# Remove extension
base = os.path.splitext(path_str)[0]
@@ -220,6 +220,18 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
return []
def normalize_newlines(multiline: str) -> str:
"""Replace any \r\n, \r, or \n with the native newline.
Args:
multiline: String containing any mixture of newlines.
Returns:
A string with normalized newlines native to the platform.
"""
return re.sub(r'\r\n?|\n', os.linesep, multiline)
def normalize_file_path_for_comparison(file_path: str) -> str:
"""Normalize a file path for conflict detection.