mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1c80d93f7 |
@@ -16,6 +16,7 @@ from basic_memory.deps import (
|
||||
FileServiceDep,
|
||||
EntityRepositoryDep,
|
||||
)
|
||||
from basic_memory.file_utils import normalize_path_separators
|
||||
from basic_memory.repository.search_repository import SearchIndexRow
|
||||
from basic_memory.schemas.memory import normalize_memory_url
|
||||
from basic_memory.schemas.search import SearchQuery, SearchItemType
|
||||
@@ -149,6 +150,9 @@ async def write_resource(
|
||||
JSON response with file information
|
||||
"""
|
||||
try:
|
||||
# Normalize file_path to prevent double slashes that cause DB conflicts (issue #431)
|
||||
file_path = normalize_path_separators(file_path)
|
||||
|
||||
# Get content from request body
|
||||
|
||||
# Defensive type checking: ensure content is a string
|
||||
|
||||
@@ -137,7 +137,32 @@ def parse_frontmatter(content: str) -> Dict[str, Any]:
|
||||
return frontmatter
|
||||
|
||||
except yaml.YAMLError as e:
|
||||
raise ParseError(f"Invalid YAML in frontmatter: {e}")
|
||||
# Provide helpful error messages for common YAML mistakes (issue #431)
|
||||
error_msg = str(e)
|
||||
suggestions = []
|
||||
|
||||
# Check for common formatting errors
|
||||
yaml_content = parts[1]
|
||||
if "could not find expected ':'" in error_msg:
|
||||
suggestions.append(
|
||||
"Missing space after colon - YAML requires 'key: value' not 'key:value'"
|
||||
)
|
||||
# Show specific line with the problem if available
|
||||
for line in yaml_content.split("\n"):
|
||||
if ":" in line and not ": " in line:
|
||||
# Found a line with colon but no space after
|
||||
suggestions.append(f"Problem line: '{line.strip()}'")
|
||||
fixed_line = line.replace(":", ": ", 1)
|
||||
suggestions.append(f"Try: '{fixed_line.strip()}'")
|
||||
break
|
||||
|
||||
if suggestions:
|
||||
suggestion_text = "\n ".join(suggestions)
|
||||
raise ParseError(
|
||||
f"Invalid YAML in frontmatter: {error_msg}\n\nSuggestions:\n {suggestion_text}"
|
||||
)
|
||||
else:
|
||||
raise ParseError(f"Invalid YAML in frontmatter: {error_msg}")
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
if not isinstance(e, ParseError):
|
||||
@@ -241,6 +266,47 @@ def sanitize_for_filename(text: str, replacement: str = "-") -> str:
|
||||
return text.strip(replacement)
|
||||
|
||||
|
||||
def normalize_path_separators(path: str) -> str:
|
||||
"""
|
||||
Normalize path separators to prevent double slashes and database conflicts.
|
||||
|
||||
This function addresses issue #431 where double slashes in paths like
|
||||
'L3_design/emotion//biorhythm.md' cause UNIQUE constraint violations
|
||||
in the database.
|
||||
|
||||
Args:
|
||||
path: File path that may contain double slashes or mixed separators
|
||||
|
||||
Returns:
|
||||
Path with normalized separators (single forward slashes)
|
||||
|
||||
Examples:
|
||||
>>> normalize_path_separators("folder//file.md")
|
||||
'folder/file.md'
|
||||
>>> normalize_path_separators("path\\\\to\\\\file.md")
|
||||
'path/to/file.md'
|
||||
>>> normalize_path_separators("./folder/./file.md")
|
||||
'folder/file.md'
|
||||
"""
|
||||
if not path:
|
||||
return ""
|
||||
|
||||
# Replace backslashes with forward slashes
|
||||
normalized = path.replace("\\", "/")
|
||||
|
||||
# Remove leading ./ if present
|
||||
if normalized.startswith("./"):
|
||||
normalized = normalized[2:]
|
||||
|
||||
# Compress multiple slashes into single slashes
|
||||
normalized = re.sub(r"/+", "/", normalized)
|
||||
|
||||
# Remove trailing slashes
|
||||
normalized = normalized.rstrip("/")
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def sanitize_for_folder(folder: str) -> str:
|
||||
"""
|
||||
Sanitize folder path to be safe for use in file system paths.
|
||||
|
||||
@@ -42,6 +42,20 @@ class MemoryJsonImporter(Importer[EntityImportResult]):
|
||||
# First pass - collect entities and relations
|
||||
for line in source_data:
|
||||
data = line
|
||||
|
||||
# Validate data structure (issue #431)
|
||||
if not isinstance(data, dict):
|
||||
logger.warning(f"Skipping invalid data (not a dict): {data}")
|
||||
skipped_entities += 1
|
||||
continue
|
||||
|
||||
if "type" not in data:
|
||||
logger.warning(
|
||||
f"Skipping data missing 'type' field. Available fields: {list(data.keys())}"
|
||||
)
|
||||
skipped_entities += 1
|
||||
continue
|
||||
|
||||
if data["type"] == "entity":
|
||||
# Handle different possible name keys
|
||||
entity_name = data.get("name") or data.get("entityName") or data.get("id")
|
||||
|
||||
@@ -24,7 +24,11 @@ from dateparser import parse
|
||||
from pydantic import BaseModel, BeforeValidator, Field, model_validator
|
||||
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.file_utils import sanitize_for_filename, sanitize_for_folder
|
||||
from basic_memory.file_utils import (
|
||||
sanitize_for_filename,
|
||||
sanitize_for_folder,
|
||||
normalize_path_separators,
|
||||
)
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@@ -238,14 +242,19 @@ class Entity(BaseModel):
|
||||
|
||||
@property
|
||||
def file_path(self):
|
||||
"""Get the file path for this entity based on its permalink."""
|
||||
"""Get the file path for this entity based on its permalink.
|
||||
|
||||
Returns normalized path to prevent double slashes (issue #431).
|
||||
"""
|
||||
safe_title = self.safe_title
|
||||
if self.content_type == "text/markdown":
|
||||
return (
|
||||
path = (
|
||||
os.path.join(self.folder, f"{safe_title}.md") if self.folder else f"{safe_title}.md"
|
||||
)
|
||||
else:
|
||||
return os.path.join(self.folder, safe_title) if self.folder else safe_title
|
||||
path = os.path.join(self.folder, safe_title) if self.folder else safe_title
|
||||
# Normalize to prevent double slashes that cause DB conflicts (issue #431)
|
||||
return normalize_path_separators(path)
|
||||
|
||||
@property
|
||||
def permalink(self) -> Optional[Permalink]:
|
||||
|
||||
@@ -588,7 +588,17 @@ class EntityService(BaseService[EntityModel]):
|
||||
# Validate count matches expected
|
||||
if actual_count != expected_replacements:
|
||||
if actual_count == 0:
|
||||
raise ValueError(f"Text to replace not found: '{find_text}'")
|
||||
# Provide helpful context for debugging (issue #431)
|
||||
# Show a snippet of the content to help diagnose the mismatch
|
||||
content_preview = current_content[:500]
|
||||
if len(current_content) > 500:
|
||||
content_preview += "..."
|
||||
|
||||
raise ValueError(
|
||||
f"Text to replace not found: '{find_text}'\n\n"
|
||||
f"Content preview (first 500 chars):\n{content_preview}\n\n"
|
||||
f"Suggestion: Use read_note() to view the full content and verify the exact text to find."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Expected {expected_replacements} occurrences of '{find_text}', "
|
||||
|
||||
@@ -272,6 +272,17 @@ def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
|
||||
if tags is None:
|
||||
return []
|
||||
|
||||
# Handle dict objects (issue #431) - PyYAML can produce dicts for malformed YAML
|
||||
# e.g., `tags:{key:value}` gets parsed as dict instead of failing
|
||||
if isinstance(tags, dict):
|
||||
logger.warning(
|
||||
f"Tags provided as dict instead of list/string. "
|
||||
f"Converting dict keys to tag list: {tags}. "
|
||||
f"Expected format: 'tags: [tag1, tag2]' or 'tags: tag1, tag2'"
|
||||
)
|
||||
# Use dict keys as tags, ignore values
|
||||
return [str(key).strip().lstrip("#") for key in tags.keys() if key]
|
||||
|
||||
# Process list of tags
|
||||
if isinstance(tags, list):
|
||||
# First strip whitespace, then strip leading '#' characters to prevent accumulation
|
||||
|
||||
Reference in New Issue
Block a user