@@ -197,6 +197,112 @@ def process_full_path(full_path: str) -> FileInfo | str | list[str]:
197197
198198 return web .json_response (results )
199199
200+ @routes .get ("/v2/userdata" )
201+ async def list_userdata_v2 (request ):
202+ """
203+ List files and directories in a user's data directory.
204+
205+ This endpoint provides a structured listing of contents within a specified
206+ subdirectory of the user's data storage.
207+
208+ Query Parameters:
209+ - path (optional): The relative path within the user's data directory
210+ to list. Defaults to the root ('').
211+
212+ Returns:
213+ - 400: If the requested path is invalid, outside the user's data directory, or is not a directory.
214+ - 404: If the requested path does not exist.
215+ - 403: If the user is invalid.
216+ - 500: If there is an error reading the directory contents.
217+ - 200: JSON response containing a list of file and directory objects.
218+ Each object includes:
219+ - name: The name of the file or directory.
220+ - type: 'file' or 'directory'.
221+ - path: The relative path from the user's data root.
222+ - size (for files): The size in bytes.
223+ - modified (for files): The last modified timestamp (Unix epoch).
224+ """
225+ requested_rel_path = request .rel_url .query .get ('path' , '' )
226+
227+ # URL-decode the path parameter
228+ try :
229+ requested_rel_path = parse .unquote (requested_rel_path )
230+ except Exception as e :
231+ logging .warning (f"Failed to decode path parameter: { requested_rel_path } , Error: { e } " )
232+ return web .Response (status = 400 , text = "Invalid characters in path parameter" )
233+
234+
235+ # Check user validity and get the absolute path for the requested directory
236+ try :
237+ base_user_path = self .get_request_user_filepath (request , None , create_dir = False )
238+
239+ if requested_rel_path :
240+ target_abs_path = self .get_request_user_filepath (request , requested_rel_path , create_dir = False )
241+ else :
242+ target_abs_path = base_user_path
243+
244+ except KeyError as e :
245+ # Invalid user detected by get_request_user_id inside get_request_user_filepath
246+ logging .warning (f"Access denied for user: { e } " )
247+ return web .Response (status = 403 , text = "Invalid user specified in request" )
248+
249+
250+ if not target_abs_path :
251+ # Path traversal or other issue detected by get_request_user_filepath
252+ return web .Response (status = 400 , text = "Invalid path requested" )
253+
254+ # Handle cases where the user directory or target path doesn't exist
255+ if not os .path .exists (target_abs_path ):
256+ # Check if it's the base user directory that's missing (new user case)
257+ if target_abs_path == base_user_path :
258+ # It's okay if the base user directory doesn't exist yet, return empty list
259+ return web .json_response ([])
260+ else :
261+ # A specific subdirectory was requested but doesn't exist
262+ return web .Response (status = 404 , text = "Requested path not found" )
263+
264+ if not os .path .isdir (target_abs_path ):
265+ return web .Response (status = 400 , text = "Requested path is not a directory" )
266+
267+ results = []
268+ try :
269+ for root , dirs , files in os .walk (target_abs_path , topdown = True ):
270+ # Process directories
271+ for dir_name in dirs :
272+ dir_path = os .path .join (root , dir_name )
273+ rel_path = os .path .relpath (dir_path , base_user_path ).replace (os .sep , '/' )
274+ results .append ({
275+ "name" : dir_name ,
276+ "path" : rel_path ,
277+ "type" : "directory"
278+ })
279+
280+ # Process files
281+ for file_name in files :
282+ file_path = os .path .join (root , file_name )
283+ rel_path = os .path .relpath (file_path , base_user_path ).replace (os .sep , '/' )
284+ entry_info = {
285+ "name" : file_name ,
286+ "path" : rel_path ,
287+ "type" : "file"
288+ }
289+ try :
290+ stats = os .stat (file_path ) # Use os.stat for potentially better performance with os.walk
291+ entry_info ["size" ] = stats .st_size
292+ entry_info ["modified" ] = stats .st_mtime
293+ except OSError as stat_error :
294+ logging .warning (f"Could not stat file { file_path } : { stat_error } " )
295+ pass # Include file with available info
296+ results .append (entry_info )
297+ except OSError as e :
298+ logging .error (f"Error listing directory { target_abs_path } : { e } " )
299+ return web .Response (status = 500 , text = "Error reading directory contents" )
300+
301+ # Sort results alphabetically, directories first then files
302+ results .sort (key = lambda x : (x ['type' ] != 'directory' , x ['name' ].lower ()))
303+
304+ return web .json_response (results )
305+
200306 def get_user_data_path (request , check_exists = False , param = "file" ):
201307 file = request .match_info .get (param , None )
202308 if not file :
0 commit comments