Files
NotesAgentic/backend/app/services/vault_paths.py
T

79 lines
2.6 KiB
Python

"""Vault 相对路径校验;所有文件操作必须先经过本模块。"""
from __future__ import annotations
import re
from pathlib import Path
from app.config import get_settings
from app.errors import ApiError
_INVALID_FILE_CHARS = re.compile(r'[\\/:*?"<>|]')
def normalize_folder(folder: str | None) -> str:
"""返回使用 `/` 的安全相对目录;根目录表示为空字符串。"""
if not folder or folder in {"/", "\\"}:
return ""
if "\x00" in folder:
raise ApiError(400, "INVALID_PATH", "folder must not contain NUL bytes")
segments: list[str] = []
for part in re.split(r"[\\/]+", folder):
if not part:
continue
if part in {".", ".."} or ":" in part:
raise ApiError(
400,
"INVALID_PATH",
"folder must be a relative path without '.' or '..' segments",
{"folder": folder},
)
segments.append(part)
return "/".join(segments)
def normalize_entry_name(name: str, *, markdown: bool = False) -> str:
"""校验单个目录项名称;不静默接受路径分隔符或保留段。"""
value = name.strip()
if not value or value in {".", ".."} or "\x00" in value:
raise ApiError(400, "INVALID_PATH", "entry name is invalid", {"name": name})
if _INVALID_FILE_CHARS.search(value):
raise ApiError(
400,
"INVALID_PATH",
"entry name contains unsupported characters",
{"name": name},
)
if markdown and not value.lower().endswith(".md"):
value += ".md"
return value
def safe_note_filename(title: str) -> str:
"""为创建笔记保留原有的宽松清洗行为。"""
value = _INVALID_FILE_CHARS.sub("_", title).strip() or "untitled"
return value if value.lower().endswith(".md") else f"{value}.md"
def resolve_in_vault(relative_path: str) -> Path:
"""把相对路径解析到当前 Vault,并拒绝符号链接/`..` 导致的越界。"""
if not relative_path or "\x00" in relative_path:
raise ApiError(
400, "INVALID_PATH", "invalid Vault-relative path", {"path": relative_path}
)
root = get_settings().vault_path.resolve()
candidate = (root / relative_path.replace("\\", "/").lstrip("/")).resolve()
if not candidate.is_relative_to(root):
raise ApiError(
400, "INVALID_PATH", "path escapes Vault", {"path": relative_path}
)
return candidate
def relative_to_vault(path: Path) -> str:
return path.resolve().relative_to(get_settings().vault_path.resolve()).as_posix()