|
9 | 9 | from core.database.postgres_database import InvalidMetadataFilterError |
10 | 10 | from core.models.auth import AuthContext |
11 | 11 | from core.models.folders import Folder, FolderCreate, FolderSummary |
12 | | -from core.models.request import FolderDetailsRequest, FolderTreeRequest |
| 12 | +from core.models.request import FolderDetailsRequest |
13 | 13 | from core.models.responses import ( |
14 | 14 | DocumentAddToFolderResponse, |
15 | 15 | DocumentDeleteResponse, |
16 | 16 | FolderDeleteResponse, |
17 | 17 | FolderDetails, |
18 | 18 | FolderDetailsResponse, |
19 | 19 | FolderDocumentInfo, |
20 | | - FolderTreeNode, |
21 | 20 | ) |
22 | 21 | from core.routes.utils import project_document_fields |
23 | 22 | from core.services.telemetry import TelemetryService |
@@ -264,176 +263,6 @@ async def list_folder_summaries(auth: AuthContext = Depends(verify_token)) -> Li |
264 | 263 | raise HTTPException(status_code=500, detail=str(exc)) |
265 | 264 |
|
266 | 265 |
|
267 | | -@router.post("/tree", response_model=FolderTreeNode) |
268 | | -async def get_folder_tree( |
269 | | - request: FolderTreeRequest, |
270 | | - auth: AuthContext = Depends(verify_token), |
271 | | -) -> FolderTreeNode: |
272 | | - """ |
273 | | - Return a hierarchical folder tree (with documents) rooted at ``folder_path``. |
274 | | -
|
275 | | - When ``folder_path`` is null or ``/``, the entire accessible hierarchy is returned. |
276 | | - """ |
277 | | - |
278 | | - try: |
279 | | - folder_path = request.folder_path |
280 | | - document_fields = request.document_fields |
281 | | - normalized_path: Optional[str] = None |
282 | | - if folder_path is not None: |
283 | | - if isinstance(folder_path, str) and folder_path.lower() == "null": |
284 | | - folder_path = None |
285 | | - else: |
286 | | - try: |
287 | | - normalized_path = normalize_folder_path(folder_path) |
288 | | - except ValueError as exc: |
289 | | - raise HTTPException(status_code=400, detail=str(exc)) |
290 | | - if normalized_path == "/": |
291 | | - normalized_path = None |
292 | | - |
293 | | - base_path = normalized_path or "/" |
294 | | - |
295 | | - base_folder: Optional[Folder] = None |
296 | | - if normalized_path: |
297 | | - base_folder = await document_service.db.get_folder_by_full_path(normalized_path, auth) |
298 | | - if not base_folder: |
299 | | - raise HTTPException(status_code=404, detail=f"Folder {folder_path} not found") |
300 | | - |
301 | | - def _canonical_folder_path(folder: Folder) -> Optional[str]: |
302 | | - if folder.full_path: |
303 | | - try: |
304 | | - return normalize_folder_path(folder.full_path) |
305 | | - except ValueError: |
306 | | - return None |
307 | | - if folder.name: |
308 | | - try: |
309 | | - return normalize_folder_path(folder.name) |
310 | | - except ValueError: |
311 | | - return None |
312 | | - return None |
313 | | - |
314 | | - def _parent_path(path: str) -> Optional[str]: |
315 | | - if not path or path == "/": |
316 | | - return None |
317 | | - segments = [part for part in path.strip("/").split("/") if part] |
318 | | - if len(segments) <= 1: |
319 | | - return "/" |
320 | | - return "/" + "/".join(segments[:-1]) |
321 | | - |
322 | | - def _attach_child(parent: FolderTreeNode, child: FolderTreeNode) -> None: |
323 | | - if not any(existing.full_path == child.full_path for existing in parent.children): |
324 | | - parent.children.append(child) |
325 | | - |
326 | | - def _make_node(path: str, folder: Optional[Folder]) -> FolderTreeNode: |
327 | | - name = folder.name if folder else ("/" if path == "/" else (path.strip("/").split("/")[-1] or "/")) |
328 | | - depth = folder.depth if folder else (0 if path == "/" else None) |
329 | | - return FolderTreeNode( |
330 | | - id=folder.id if folder else None, |
331 | | - name=name, |
332 | | - full_path=path, |
333 | | - description=folder.description if folder else None, |
334 | | - depth=depth, |
335 | | - documents=[], |
336 | | - children=[], |
337 | | - ) |
338 | | - |
339 | | - all_folders = await document_service.db.list_folders(auth) |
340 | | - folders_with_paths: List[tuple[str, Folder]] = [] |
341 | | - for folder in all_folders: |
342 | | - path = _canonical_folder_path(folder) |
343 | | - if path: |
344 | | - folders_with_paths.append((path, folder)) |
345 | | - |
346 | | - if normalized_path: |
347 | | - scoped = [] |
348 | | - prefix = normalized_path.rstrip("/") + "/" |
349 | | - for path, folder in folders_with_paths: |
350 | | - if path == normalized_path or path.startswith(prefix): |
351 | | - scoped.append((path, folder)) |
352 | | - folders_with_paths = scoped |
353 | | - if base_folder: |
354 | | - base_folder_path = _canonical_folder_path(base_folder) |
355 | | - if base_folder_path and all(path != base_folder_path for path, _ in folders_with_paths): |
356 | | - folders_with_paths.append((base_folder_path, base_folder)) |
357 | | - |
358 | | - nodes_by_path: Dict[str, FolderTreeNode] = { |
359 | | - path: _make_node(path, folder) for path, folder in folders_with_paths |
360 | | - } |
361 | | - |
362 | | - root_node = nodes_by_path.get(base_path) |
363 | | - if not root_node: |
364 | | - root_node = _make_node(base_path, base_folder) |
365 | | - nodes_by_path[base_path] = root_node |
366 | | - |
367 | | - for path in sorted(nodes_by_path.keys(), key=lambda p: (p.count("/"), p)): |
368 | | - if path == base_path: |
369 | | - continue |
370 | | - node = nodes_by_path[path] |
371 | | - parent_path = _parent_path(path) |
372 | | - parent_node = nodes_by_path.get(parent_path) |
373 | | - if not parent_node: |
374 | | - parent_node = root_node |
375 | | - _attach_child(parent_node, node) |
376 | | - |
377 | | - doc_system_filters = {"folder_path_prefix": base_path} if normalized_path else None |
378 | | - document_result = await document_service.db.list_documents_flexible( |
379 | | - auth=auth, |
380 | | - skip=0, |
381 | | - limit=None, |
382 | | - system_filters=doc_system_filters, |
383 | | - include_total_count=False, |
384 | | - include_status_counts=False, |
385 | | - include_folder_counts=False, |
386 | | - return_documents=True, |
387 | | - sort_by="filename", |
388 | | - sort_direction="asc", |
389 | | - ) |
390 | | - |
391 | | - documents = document_result.get("documents", []) or [] |
392 | | - for document in documents: |
393 | | - if hasattr(document, "model_dump"): |
394 | | - doc_dict = document.model_dump(mode="json") |
395 | | - elif hasattr(document, "dict"): |
396 | | - doc_dict = document.dict() |
397 | | - else: |
398 | | - doc_dict = dict(document) |
399 | | - |
400 | | - doc_path_raw = doc_dict.get("folder_path") |
401 | | - try: |
402 | | - doc_path = normalize_folder_path(doc_path_raw) if doc_path_raw is not None else None |
403 | | - except ValueError: |
404 | | - doc_path = doc_path_raw |
405 | | - |
406 | | - target_path = doc_path or base_path |
407 | | - target_node = nodes_by_path.get(target_path) |
408 | | - if not target_node: |
409 | | - target_node = _make_node(target_path, None) |
410 | | - nodes_by_path[target_path] = target_node |
411 | | - parent_path = _parent_path(target_path or "/") |
412 | | - parent_node = nodes_by_path.get(parent_path) or root_node |
413 | | - _attach_child(parent_node, target_node) |
414 | | - |
415 | | - projected_doc = project_document_fields(doc_dict, document_fields) |
416 | | - if doc_path_raw is not None and "folder_path" not in projected_doc: |
417 | | - projected_doc["folder_path"] = doc_path_raw |
418 | | - |
419 | | - target_node.documents.append(projected_doc) |
420 | | - |
421 | | - def _sort_tree(node: FolderTreeNode) -> None: |
422 | | - node.children.sort(key=lambda child: (child.name or "", child.full_path or "")) |
423 | | - for child in node.children: |
424 | | - _sort_tree(child) |
425 | | - node.documents.sort(key=lambda doc: str(doc.get("filename") or doc.get("external_id") or "")) |
426 | | - |
427 | | - _sort_tree(root_node) |
428 | | - return root_node |
429 | | - |
430 | | - except HTTPException: |
431 | | - raise |
432 | | - except Exception as exc: # noqa: BLE001 |
433 | | - logger.error("Error building folder tree: %s", exc) |
434 | | - raise HTTPException(status_code=500, detail=str(exc)) |
435 | | - |
436 | | - |
437 | 266 | @router.post("/{folder_id_or_name:path}/documents/{document_id}", response_model=DocumentAddToFolderResponse) |
438 | 267 | @telemetry.track(operation_type="add_document_to_folder", metadata_resolver=telemetry.add_document_to_folder_metadata) |
439 | 268 | async def add_document_to_folder( |
|
0 commit comments