فهرست منبع

Add new endpoint for shallow file listing - backend only (#1174)

* Add /api/list-files endpoint

Returns files at the path specified by query parameter relpath, relative to base path, with a depth of 1

* Add method in services
RaGe 1 سال پیش
والد
کامیت
2bf34093b0
3فایلهای تغییر یافته به همراه64 افزوده شده و 1 حذف شده
  1. 18 0
      frontend/src/services/fileService.ts
  2. 27 0
      opendevin/files.py
  3. 19 1
      opendevin/server/listen.py

+ 18 - 0
frontend/src/services/fileService.ts

@@ -17,3 +17,21 @@ export async function getWorkspace(): Promise<WorkspaceFile> {
   const data = await res.json();
   return data as WorkspaceFile;
 }
+
+export type WorkspaceItem = {
+  name: string;
+  isBranch: boolean;
+  relativePath: string;
+  id: string;
+  parent: string | null;
+  children: string[];
+};
+
+export async function getWorkspaceDepthOne(
+  relpath: string,
+): Promise<WorkspaceItem[]> {
+  const res = await fetch(`/api/list-files?relpath=${relpath}`);
+  const json = await res.json();
+  const files = json.files as WorkspaceItem[];
+  return files;
+}

+ 27 - 0
opendevin/files.py

@@ -1,5 +1,6 @@
 from pathlib import Path
 from typing import Any, Dict, List
+from pydantic import BaseModel
 
 
 class WorkspaceFile:
@@ -40,3 +41,29 @@ def get_folder_structure(workdir: Path) -> WorkspaceFile:
         else:
             root.children.append(WorkspaceFile(name=item.name, children=[]))
     return root
+
+
+class WorkspaceItem(BaseModel):
+    name: str
+    isBranch: bool
+    relativePath: str
+    id: str
+    parent: str | None
+    children: List['WorkspaceItem'] = []
+
+
+def get_single_level_folder_structure(base_path: Path, workdir: Path) -> List[WorkspaceItem]:
+    """Generate a list of files and directories at the current level with type indicator, relative paths, and tree metadata."""
+    entries = []
+    for item in workdir.iterdir():
+        item_relative_path = item.relative_to(base_path).as_posix()
+        # Using the relative path as an 'id' ensuring uniqueness within the workspace context
+        parent_path = workdir.relative_to(base_path).as_posix() if workdir != base_path else 'root'
+        entries.append(WorkspaceItem(
+            name=item.name,
+            isBranch=item.is_dir(),
+            relativePath=item_relative_path,
+            id=item_relative_path,
+            parent=parent_path
+        ))
+    return entries

+ 19 - 1
opendevin/server/listen.py

@@ -2,7 +2,7 @@ import uuid
 from pathlib import Path
 
 import litellm
-from fastapi import Depends, FastAPI, WebSocket, status
+from fastapi import Depends, FastAPI, WebSocket, HTTPException, Query, status
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.staticfiles import StaticFiles
@@ -116,6 +116,24 @@ def refresh_files():
     return structure.to_dict()
 
 
+@app.get('/api/list-files')
+def list_files(relpath: str = Query(None, description='Relative path from workspace base')):
+    """Refreshes and returns the files and directories from a specified subdirectory or the base directory if no subdirectory is specified, limited to one level deep."""
+    base_path = Path(config.get('WORKSPACE_BASE')).resolve()
+    full_path = (base_path / relpath).resolve() if relpath is not None else base_path
+
+    logger.debug(f'Listing files at {full_path}')
+
+    # Ensure path exists, is a directory,
+    # And is within the workspace base directory - to prevent directory traversal attacks
+    # https://owasp.org/www-community/attacks/Path_Traversal
+    if not full_path.exists() or not full_path.is_dir() or not str(full_path).startswith(str(base_path)):
+        raise HTTPException(status_code=400, detail='Invalid path provided.')
+
+    structure = files.get_single_level_folder_structure(base_path, full_path)
+    return {'files': structure}
+
+
 @app.get('/api/select-file')
 def select_file(file: str):
     try: