feat: vault DIR — exclusions par vault et lecture seule déclarable (vault Spark)
This commit is contained in:
@@ -7,6 +7,7 @@ Single source of truth consumed by both the REST routes (``/api/vaults``,
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
from datetime import UTC
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -15,6 +16,68 @@ from backend.services.errors import ServiceError
|
||||
from backend.services.paths import resolve_safe_path
|
||||
|
||||
|
||||
def chemin_exclu_dans_config(config: dict[str, Any] | None, rel_path: str) -> bool:
|
||||
"""True si *rel_path* est exclu par la config d'exclusions d'un vault.
|
||||
|
||||
Deux mécanismes, lus dans le dict ``config`` du vault (alimenté par
|
||||
``DIR_N_IGNORED_DIRS`` / ``DIR_N_IGNORED_PATHS``, vaults DIR seulement) :
|
||||
|
||||
- ``ignoredDirs`` : le nom d'un segment du chemin (à toute profondeur)
|
||||
figure dans l'ensemble — ex. ``.home`` cache ``a/.home/x`` ;
|
||||
- ``ignoredPaths`` : motifs fnmatch testés contre le chemin relatif
|
||||
POSIX **et chacun de ses préfixes** — un dossier qui matche cache
|
||||
donc tout son sous-arbre, ex. ``Executeurs/*/data`` cache aussi
|
||||
``Executeurs/Hermes/data/config.md``.
|
||||
|
||||
Tolérant : config absente/vide ou chemin vide → False.
|
||||
"""
|
||||
if not config or not rel_path:
|
||||
return False
|
||||
rel = str(rel_path).replace("\\", "/").strip("/")
|
||||
if not rel:
|
||||
return False
|
||||
segments = rel.split("/")
|
||||
ignored_dirs = config.get("ignoredDirs") or []
|
||||
if any(seg in ignored_dirs for seg in segments):
|
||||
return True
|
||||
prefixes = ["/".join(segments[:i]) for i in range(1, len(segments) + 1)]
|
||||
for pattern in config.get("ignoredPaths") or []:
|
||||
pat = str(pattern).replace("\\", "/").strip("/")
|
||||
if pat and any(fnmatch.fnmatchcase(prefix, pat) for prefix in prefixes):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def chemin_exclu(vault_name: str, rel_path: str) -> bool:
|
||||
"""True si *rel_path* est exclu dans le vault *vault_name*.
|
||||
|
||||
Lit la config du vault via l'index (``get_vault_data``). Tolérant :
|
||||
vault inconnu ou sans config d'exclusions → False.
|
||||
"""
|
||||
try:
|
||||
from backend.indexer import get_vault_data
|
||||
|
||||
data = get_vault_data(vault_name)
|
||||
except Exception:
|
||||
return False
|
||||
if not data:
|
||||
return False
|
||||
return chemin_exclu_dans_config(data.get("config") or {}, rel_path)
|
||||
|
||||
|
||||
def vault_readonly(vault_name: str) -> bool:
|
||||
"""True si le vault est déclaré en lecture seule (DIR_N_READONLY)."""
|
||||
try:
|
||||
from backend.indexer import get_vault_data
|
||||
|
||||
data = get_vault_data(vault_name)
|
||||
except Exception:
|
||||
return False
|
||||
if not data:
|
||||
return False
|
||||
return bool((data.get("config") or {}).get("readonly", False))
|
||||
|
||||
|
||||
def list_accessible_vaults(user: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Return the vaults *user* may access, with summary metadata."""
|
||||
from backend.auth.middleware import check_vault_access
|
||||
@@ -29,6 +92,7 @@ def list_accessible_vaults(user: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"file_count": len(data.get("files", [])),
|
||||
"tag_count": len(data.get("tags", {})),
|
||||
"type": data.get("config", {}).get("type", "VAULT"),
|
||||
"readonly": bool(data.get("config", {}).get("readonly", False)),
|
||||
})
|
||||
return result
|
||||
|
||||
@@ -58,7 +122,7 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
|
||||
root = get_vault_root(vault_name)
|
||||
target = resolve_safe_path(root, path) if path else root.resolve()
|
||||
|
||||
if not target.exists():
|
||||
if not target.exists() or (path and chemin_exclu(vault_name, path)):
|
||||
raise ServiceError(
|
||||
f"Path not found: {path}",
|
||||
code="not_found",
|
||||
@@ -74,12 +138,15 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
|
||||
if hide_hidden and entry.name.startswith("."):
|
||||
continue
|
||||
rel = str(entry.relative_to(root)).replace("\\", "/")
|
||||
if chemin_exclu(vault_name, rel):
|
||||
continue
|
||||
if entry.is_dir():
|
||||
# Count only direct children (files and subdirs) for performance.
|
||||
try:
|
||||
file_count = sum(
|
||||
1 for child in entry.iterdir()
|
||||
if (not hide_hidden or not child.name.startswith("."))
|
||||
and not chemin_exclu(vault_name, f"{rel}/{child.name}")
|
||||
and (child.is_file() and (child.suffix.lower() in SUPPORTED_EXTENSIONS or child.name.lower() in ("dockerfile", "makefile"))
|
||||
or child.is_dir())
|
||||
)
|
||||
@@ -200,6 +267,9 @@ def list_all_files(
|
||||
stat = entry.stat()
|
||||
rel_path = str(entry.relative_to(root)).replace("\\", "/")
|
||||
|
||||
if chemin_exclu(vault_name, rel_path):
|
||||
continue
|
||||
|
||||
if recursive and dir_prefix:
|
||||
try:
|
||||
rel_to_dir = str(entry.parent.relative_to(dir_path)).replace("\\", "/")
|
||||
|
||||
Reference in New Issue
Block a user