feat(ai): catalogue d'outils lecture & recherche (#79 phase C)
CI / lint (push) Successful in 59s
CI / security (push) Successful in 49s
CI / test (push) Successful in 1m24s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m24s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

This commit is contained in:
2026-09-11 20:33:15 -04:00
parent 7cf7d33b6d
commit 31b65ade5b
13 changed files with 1220 additions and 524 deletions
+112
View File
@@ -100,3 +100,115 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
raise ServiceError("Permission denied", code="permission_denied", status=403) from None
return {"vault": vault_name, "path": path, "items": items}
def list_all_files(
vault_name: str,
dir: str = "",
limit: int = 200,
recursive: bool = True,
) -> dict[str, Any]:
"""List files in a vault directory sorted by modification time (newest first).
Unlike :func:`browse_directory`, this returns files only (with metadata:
size, mtime, extension) and can recurse into subdirectories. Hidden files
and ignored directories are skipped.
Args:
vault_name: Name of the vault.
dir: Relative directory path within the vault (empty = root).
limit: Maximum number of files to return.
recursive: Recurse into subdirectories when True.
Returns:
Dict with ``vault``, ``directory``, ``recursive``, ``count`` and
``files``.
Raises:
ServiceError: ``not_found`` (404) if the directory does not exist,
``permission_denied`` (403) if it cannot be read.
"""
from datetime import datetime, timezone
from backend.indexer import IGNORED_DIRS
root = get_vault_root(vault_name)
dir_path = resolve_safe_path(root, dir) if dir else root
if not dir_path.exists() or not dir_path.is_dir():
raise ServiceError(
f"Directory not found: {dir}",
code="not_found",
status=404,
details={"vault": vault_name, "path": dir},
)
files: list[dict[str, Any]] = []
dir_prefix = (dir or "").strip("/")
try:
iterator = dir_path.rglob("*") if recursive else dir_path.iterdir()
for entry in iterator:
if not entry.is_file():
continue
if entry.name.startswith("."):
continue
if entry.name in IGNORED_DIRS:
continue
if recursive and dir_prefix:
skip = False
try:
for part in entry.relative_to(dir_path).parts[:-1]:
if part.startswith(".") or part in IGNORED_DIRS:
skip = True
break
except ValueError:
pass
if skip:
continue
stat = entry.stat()
rel_path = str(entry.relative_to(root)).replace("\\", "/")
if recursive and dir_prefix:
try:
rel_to_dir = str(entry.parent.relative_to(dir_path)).replace("\\", "/")
except ValueError:
rel_to_dir = ""
else:
rel_to_dir = ""
ext = entry.suffix.lower() if entry.suffix else ""
file_entry = {
"name": entry.name,
"path": rel_path,
"vault": vault_name,
"size": stat.st_size,
"modified": stat.st_mtime,
"modified_iso": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
"extension": ext.lstrip(".") if ext else "",
}
if rel_to_dir and rel_to_dir != ".":
file_entry["rel_dir"] = rel_to_dir
files.append(file_entry)
except PermissionError:
raise ServiceError(
"Permission denied reading directory",
code="permission_denied",
status=403,
details={"vault": vault_name, "path": dir},
) from None
files.sort(key=lambda f: f["modified"], reverse=True)
files = files[:limit]
return {
"vault": vault_name,
"directory": dir,
"recursive": recursive,
"count": len(files),
"files": files,
}