feat(workspace): 接入真实Vault数据链路
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user