mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(sidebar): show empty folders and create folders from the sidebar (#999)
Empty folders (tracked in the scan-recorded all_folders list, same source
the move/download destination picker uses) can now be surfaced in the
folder sidebar via a view-options toggle, dimmed when their subtree holds
no models. Folders can be created directly from the sidebar through a new
POST /api/lm/{prefix}/create-folder endpoint with library-root
containment checks; the scanner records the new directory incrementally
so the tree reflects it without a rescan.
The sidebar header moves its view toggles (tree/list, recursive, empty
folders) into a "..." menu to fit the new create-folder button.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""Tests for ModelMoveService folder creation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from py.services.model_file_service import ModelMoveService
|
||||
|
||||
|
||||
class FakeScanner:
|
||||
def __init__(self, roots: List[Path]) -> None:
|
||||
self._roots = [str(root) for root in roots]
|
||||
self.known_folders: List[str] = []
|
||||
|
||||
def get_model_roots(self) -> List[str]:
|
||||
return list(self._roots)
|
||||
|
||||
async def add_known_folder(self, folder: str) -> None:
|
||||
self.known_folders.append(folder)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_folder_creates_directory_and_registers_it(tmp_path: Path):
|
||||
scanner = FakeScanner([tmp_path])
|
||||
service = ModelMoveService(scanner, "lora")
|
||||
|
||||
target = tmp_path / "characters" / "anime"
|
||||
result = await service.create_folder(str(target))
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["created"] is True
|
||||
assert result["folder"] == "characters/anime"
|
||||
assert target.is_dir()
|
||||
assert scanner.known_folders == ["characters/anime"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_folder_existing_directory_reports_not_created(tmp_path: Path):
|
||||
(tmp_path / "existing").mkdir()
|
||||
scanner = FakeScanner([tmp_path])
|
||||
service = ModelMoveService(scanner, "lora")
|
||||
|
||||
result = await service.create_folder(str(tmp_path / "existing"))
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["created"] is False
|
||||
assert result["folder"] == "existing"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_folder_rejects_paths_outside_roots(tmp_path: Path):
|
||||
root = tmp_path / "library"
|
||||
root.mkdir()
|
||||
scanner = FakeScanner([root])
|
||||
service = ModelMoveService(scanner, "lora")
|
||||
|
||||
outside = tmp_path / "outside"
|
||||
result = await service.create_folder(str(outside))
|
||||
|
||||
assert result["success"] is False
|
||||
assert "error" in result
|
||||
assert not outside.exists()
|
||||
assert scanner.known_folders == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_folder_rejects_traversal_outside_roots(tmp_path: Path):
|
||||
root = tmp_path / "library"
|
||||
root.mkdir()
|
||||
scanner = FakeScanner([root])
|
||||
service = ModelMoveService(scanner, "lora")
|
||||
|
||||
result = await service.create_folder(str(root / ".." / "escape"))
|
||||
|
||||
assert result["success"] is False
|
||||
assert not (tmp_path / "escape").exists()
|
||||
assert scanner.known_folders == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_folder_requires_path(tmp_path: Path):
|
||||
service = ModelMoveService(FakeScanner([tmp_path]), "lora")
|
||||
|
||||
result = await service.create_folder("")
|
||||
|
||||
assert result["success"] is False
|
||||
@@ -1500,6 +1500,53 @@ async def test_get_all_folders_backfills_when_never_recorded(tmp_path: Path):
|
||||
assert "empty" in all_folders
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_known_folder_records_folder_and_parents(tmp_path: Path):
|
||||
_create_files(tmp_path)
|
||||
scanner = DummyScanner(tmp_path)
|
||||
await scanner._initialize_cache()
|
||||
cache = await scanner.get_cached_data()
|
||||
|
||||
await scanner.add_known_folder("new/nested")
|
||||
|
||||
assert "new" in cache.all_folders
|
||||
assert "new/nested" in cache.all_folders
|
||||
# Models-only folder list is unaffected by empty directory creation
|
||||
assert "new" not in cache.folders
|
||||
# Idempotent: recording the same folder again keeps the list stable
|
||||
await scanner.add_known_folder("new/nested")
|
||||
assert cache.all_folders.count("new") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_known_folder_noop_without_recorded_folders(tmp_path: Path):
|
||||
_create_files(tmp_path)
|
||||
scanner = DummyScanner(tmp_path)
|
||||
await scanner._initialize_cache()
|
||||
cache = await scanner.get_cached_data()
|
||||
cache.all_folders = None
|
||||
|
||||
# Legacy snapshot without recorded folders: the scheduled backfill walk
|
||||
# discovers the directory from disk instead.
|
||||
await scanner.add_known_folder("new")
|
||||
|
||||
assert cache.all_folders is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_known_folder_ignores_empty_input(tmp_path: Path):
|
||||
_create_files(tmp_path)
|
||||
scanner = DummyScanner(tmp_path)
|
||||
await scanner._initialize_cache()
|
||||
cache = await scanner.get_cached_data()
|
||||
before = list(cache.all_folders)
|
||||
|
||||
await scanner.add_known_folder("")
|
||||
await scanner.add_known_folder("/")
|
||||
|
||||
assert cache.all_folders == before
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_folders_updated_after_move(tmp_path: Path):
|
||||
first, _, _ = _create_files(tmp_path)
|
||||
|
||||
Reference in New Issue
Block a user