33import fnmatch
44import logging
55import os
6- from typing import Dict , List , Optional
6+ from typing import Dict , List , Optional , Sequence
77
8+ from basic_memory .models import Entity
89from basic_memory .repository import EntityRepository
910from basic_memory .schemas .directory import DirectoryNode
1011
@@ -89,6 +90,49 @@ async def get_directory_tree(self) -> DirectoryNode:
8990 # Return the root node with its children
9091 return root_node
9192
93+ async def get_directory_structure (self ) -> DirectoryNode :
94+ """Build a hierarchical directory structure without file details.
95+
96+ Optimized method for folder navigation that only returns directory nodes,
97+ no file metadata. Much faster than get_directory_tree() for large knowledge bases.
98+
99+ Returns:
100+ DirectoryNode tree containing only folders (type="directory")
101+ """
102+ # Get unique directories without loading entities
103+ directories = await self .entity_repository .get_distinct_directories ()
104+
105+ # Create a root directory node
106+ root_node = DirectoryNode (name = "Root" , directory_path = "/" , type = "directory" )
107+
108+ # Map to store directory nodes by path for easy lookup
109+ dir_map : Dict [str , DirectoryNode ] = {"/" : root_node }
110+
111+ # Build tree with just folders
112+ for dir_path in directories :
113+ parts = [p for p in dir_path .split ("/" ) if p ]
114+ current_path = "/"
115+
116+ for i , part in enumerate (parts ):
117+ parent_path = current_path
118+ # Build the directory path
119+ current_path = (
120+ f"{ current_path } { part } " if current_path == "/" else f"{ current_path } /{ part } "
121+ )
122+
123+ # Create directory node if it doesn't exist
124+ if current_path not in dir_map :
125+ dir_node = DirectoryNode (
126+ name = part , directory_path = current_path , type = "directory"
127+ )
128+ dir_map [current_path ] = dir_node
129+
130+ # Add to parent's children
131+ if parent_path in dir_map :
132+ dir_map [parent_path ].children .append (dir_node )
133+
134+ return root_node
135+
92136 async def list_directory (
93137 self ,
94138 dir_name : str = "/" ,
@@ -118,8 +162,13 @@ async def list_directory(
118162 if dir_name != "/" and dir_name .endswith ("/" ):
119163 dir_name = dir_name .rstrip ("/" )
120164
121- # Get the full directory tree
122- root_tree = await self .get_directory_tree ()
165+ # Optimize: Query only entities in the target directory
166+ # instead of loading the entire tree
167+ dir_prefix = dir_name .lstrip ("/" )
168+ entity_rows = await self .entity_repository .find_by_directory_prefix (dir_prefix )
169+
170+ # Build a partial tree from only the relevant entities
171+ root_tree = self ._build_directory_tree_from_entities (entity_rows , dir_name )
123172
124173 # Find the target directory node
125174 target_node = self ._find_directory_node (root_tree , dir_name )
@@ -132,6 +181,78 @@ async def list_directory(
132181
133182 return result
134183
184+ def _build_directory_tree_from_entities (
185+ self , entity_rows : Sequence [Entity ], root_path : str
186+ ) -> DirectoryNode :
187+ """Build a directory tree from a subset of entities.
188+
189+ Args:
190+ entity_rows: Sequence of entity objects to build tree from
191+ root_path: Root directory path for the tree
192+
193+ Returns:
194+ DirectoryNode representing the tree root
195+ """
196+ # Create a root directory node
197+ root_node = DirectoryNode (name = "Root" , directory_path = root_path , type = "directory" )
198+
199+ # Map to store directory nodes by path for easy lookup
200+ dir_map : Dict [str , DirectoryNode ] = {root_path : root_node }
201+
202+ # First pass: create all directory nodes
203+ for file in entity_rows :
204+ # Process directory path components
205+ parts = [p for p in file .file_path .split ("/" ) if p ]
206+
207+ # Create directory structure
208+ current_path = "/"
209+ for i , part in enumerate (parts [:- 1 ]): # Skip the filename
210+ parent_path = current_path
211+ # Build the directory path
212+ current_path = (
213+ f"{ current_path } { part } " if current_path == "/" else f"{ current_path } /{ part } "
214+ )
215+
216+ # Create directory node if it doesn't exist
217+ if current_path not in dir_map :
218+ dir_node = DirectoryNode (
219+ name = part , directory_path = current_path , type = "directory"
220+ )
221+ dir_map [current_path ] = dir_node
222+
223+ # Add to parent's children
224+ if parent_path in dir_map :
225+ dir_map [parent_path ].children .append (dir_node )
226+
227+ # Second pass: add file nodes to their parent directories
228+ for file in entity_rows :
229+ file_name = os .path .basename (file .file_path )
230+ parent_dir = os .path .dirname (file .file_path )
231+ directory_path = "/" if parent_dir == "" else f"/{ parent_dir } "
232+
233+ # Create file node
234+ file_node = DirectoryNode (
235+ name = file_name ,
236+ file_path = file .file_path ,
237+ directory_path = f"/{ file .file_path } " ,
238+ type = "file" ,
239+ title = file .title ,
240+ permalink = file .permalink ,
241+ entity_id = file .id ,
242+ entity_type = file .entity_type ,
243+ content_type = file .content_type ,
244+ updated_at = file .updated_at ,
245+ )
246+
247+ # Add to parent directory's children
248+ if directory_path in dir_map :
249+ dir_map [directory_path ].children .append (file_node )
250+ elif root_path in dir_map :
251+ # Fallback to root if parent not found
252+ dir_map [root_path ].children .append (file_node )
253+
254+ return root_node
255+
135256 def _find_directory_node (
136257 self , root : DirectoryNode , target_path : str
137258 ) -> Optional [DirectoryNode ]:
0 commit comments