diff --git a/app/user_manager.py b/app/user_manager.py index e7381e621d98..4b1ef08b7c14 100644 --- a/app/user_manager.py +++ b/app/user_manager.py @@ -146,6 +146,7 @@ async def listuserdata(request): - recurse (optional): If "true", recursively list files in subdirectories. - full_info (optional): If "true", return detailed file information (path, size, modified time). - split (optional): If "true", split file paths into components (only applies when full_info is false). + - emptyDirs (optional): If "true", include empty directories in the listing. Returns: - 400: If 'dir' parameter is missing. @@ -172,6 +173,7 @@ async def listuserdata(request): recurse = request.rel_url.query.get('recurse', '').lower() == "true" full_info = request.rel_url.query.get('full_info', '').lower() == "true" split_path = request.rel_url.query.get('split', '').lower() == "true" + include_empty_dirs = request.rel_url.query.get('emptyDirs', '').lower() == "true" # Use different patterns based on whether we're recursing or not if recurse: @@ -185,15 +187,29 @@ def process_full_path(full_path: str) -> FileInfo | str | list[str]: rel_path = os.path.relpath(full_path, path).replace(os.sep, '/') if split_path: - return [rel_path] + rel_path.split('/') + if os.path.isdir(full_path): + dirname, basename = rel_path, "" + else: + head, tail = rel_path.rsplit('/', 1) if '/' in rel_path else ("", rel_path) + dirname, basename = head, tail + return [rel_path, dirname, basename] return rel_path - results = [ - process_full_path(full_path) - for full_path in glob.glob(pattern, recursive=recurse) - if os.path.isfile(full_path) - ] + enum_entries = glob.glob(pattern, recursive=recurse) + results = [] + for full_path in enum_entries: + is_dir = os.path.isdir(full_path) + + if is_dir: + # skip every dir unless we're explicitly including empty ones + if not include_empty_dirs: + continue + # when including dirs, only keep the empty ones + if os.listdir(full_path): + continue + + results.append(process_full_path(full_path)) return web.json_response(results) diff --git a/tests-unit/prompt_server_test/user_manager_test.py b/tests-unit/prompt_server_test/user_manager_test.py index 7e523cbf486c..86c254a1cc8f 100644 --- a/tests-unit/prompt_server_test/user_manager_test.py +++ b/tests-unit/prompt_server_test/user_manager_test.py @@ -117,6 +117,124 @@ async def test_listuserdata_normalized_separator(aiohttp_client, app, tmp_path): assert "\\" not in result[0]["path"] # Ensure backslash is not present assert result[0]["path"] == "subdir/file1.txt" +async def test_listuserdata_include_empty_dirs(aiohttp_client, app, tmp_path): + # Arrange + test_dir = tmp_path / "test_dir" + empty_subdir = test_dir / "empty_subdir" + file1 = test_dir / "file1.txt" + + os.makedirs(test_dir) + os.makedirs(empty_subdir) + with open(file1, "w") as f: + f.write("test") + + client = await aiohttp_client(app) + + # Act + resp = await client.get("/userdata?dir=test_dir&emptyDirs=true") + + # Assert + assert resp.status == 200 + result = await resp.json() + assert set(result) == {"file1.txt", "empty_subdir"} + +async def test_listuserdata_exclude_empty_dirs_default(aiohttp_client, app, tmp_path): + # Arrange + test_dir = tmp_path / "test_dir" + empty_subdir = test_dir / "empty_subdir" + file1 = test_dir / "file1.txt" + + os.makedirs(test_dir) + os.makedirs(empty_subdir) + with open(file1, "w") as f: + f.write("test") + + client = await aiohttp_client(app) + + # Act + resp = await client.get("/userdata?dir=test_dir") # emptyDirs defaults to false + + # Assert + assert resp.status == 200 + result = await resp.json() + assert result == ["file1.txt"] + +async def test_listuserdata_recursive_include_empty_dirs(aiohttp_client, app, tmp_path): + # Arrange + base_dir = tmp_path / "test_dir" + occupied = base_dir / "occupied_directory" + empty = base_dir / "empty_directory" + file1 = occupied / "file1.txt" + + os.makedirs(occupied) + os.makedirs(empty) + with open(file1, "w") as f: + f.write("content") + + client = await aiohttp_client(app) + + # Act + resp = await client.get("/userdata?dir=test_dir&recurse=true&emptyDirs=true") + + # Assert + assert resp.status == 200 + result = await resp.json() + assert set(result) == {"occupied_directory/file1.txt", "empty_directory"} + +async def test_listuserdata_full_info_include_empty_dirs(aiohttp_client, app, tmp_path): + # Arrange + test_dir = tmp_path / "test_dir" + file1 = test_dir / "file1.txt" + empty = test_dir / "empty_subdir" + os.makedirs(test_dir) + os.makedirs(empty) + with open(file1, "w") as f: + f.write("content") + + client = await aiohttp_client(app) + + # Act + resp = await client.get("/userdata?dir=test_dir&full_info=true&emptyDirs=true") + + # Assert + assert resp.status == 200 + result = await resp.json() + paths = {info["path"] for info in result} + assert paths == {"file1.txt", "empty_subdir"} + for info in result: + assert "size" in info + assert "modified" in info + +async def test_listuserdata_recurse_split_include_empty_dirs(aiohttp_client, app, tmp_path): + # Arrange + test_dir = tmp_path / "test_dir" + file1 = test_dir / "file1.txt" + empty = test_dir / "empty_subdir" + occupying_dir = test_dir / "occupied_directory" + another_occupying_dir = occupying_dir / "another_occupied_directory" + file2 = another_occupying_dir / "file2.txt" + os.makedirs(test_dir) + os.makedirs(occupying_dir) + os.makedirs(another_occupying_dir) + os.makedirs(empty) + with open(file1, "w") as f: + f.write("content") + with open(file2, "w") as f: + f.write("nested content") + + client = await aiohttp_client(app) + + # Act + resp = await client.get("/userdata?dir=test_dir&split=true&emptyDirs=true&recurse=true") + + # Assert + assert resp.status == 200 + result = await resp.json() + assert set(tuple(r) for r in result) == { + ("file1.txt", "", "file1.txt"), + ("empty_subdir", "empty_subdir", ""), + ("occupied_directory/another_occupied_directory/file2.txt", "occupied_directory/another_occupied_directory", "file2.txt"), + } async def test_post_userdata_new_file(aiohttp_client, app, tmp_path): client = await aiohttp_client(app)