mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-19 08:52:05 -03:00
fix(example-images): skip hidden files in path validation, show offending items on failure (#807)
This commit is contained in:
@@ -49,7 +49,10 @@ from ...utils.constants import (
|
||||
VALID_LORA_TYPES,
|
||||
)
|
||||
from ...utils.civitai_utils import rewrite_preview_url
|
||||
from ...utils.example_images_paths import is_valid_example_images_root
|
||||
from ...utils.example_images_paths import (
|
||||
find_non_compliant_items_in_example_images_root,
|
||||
is_valid_example_images_root,
|
||||
)
|
||||
from ...utils.lora_metadata import extract_trained_words
|
||||
from ...utils.session_logging import get_standalone_session_log_snapshot
|
||||
from ...utils.usage_stats import UsageStats
|
||||
@@ -1498,6 +1501,16 @@ class SettingsHandler:
|
||||
if not os.path.isdir(folder_path):
|
||||
return "Please set a dedicated folder for example images."
|
||||
if not self._is_dedicated_example_images_folder(folder_path):
|
||||
offending = find_non_compliant_items_in_example_images_root(folder_path)
|
||||
if offending:
|
||||
items_str = ", ".join(repr(item) for item in offending[:5])
|
||||
if len(offending) > 5:
|
||||
items_str += f" … and {len(offending) - 5} more"
|
||||
return (
|
||||
f"The folder contains items that are not valid example image "
|
||||
f"folders: {items_str}. Please use a dedicated, empty folder "
|
||||
f"for example images to prevent accidental data loss."
|
||||
)
|
||||
return "Please set a dedicated folder for example images."
|
||||
return None
|
||||
|
||||
|
||||
@@ -12,6 +12,18 @@ from ..services.settings_manager import get_settings_manager
|
||||
|
||||
_HEX_PATTERN = re.compile(r"[a-fA-F0-9]{64}")
|
||||
|
||||
# Filesystem/metadata files that are never created by the example images system
|
||||
# and are safe to ignore during validation. The cleanup service only operates on
|
||||
# directories, so these files pose no data-loss risk.
|
||||
_SAFE_FILENAMES: frozenset[str] = frozenset({
|
||||
".DS_Store", # macOS folder metadata
|
||||
"Thumbs.db", # Windows thumbnail cache
|
||||
"desktop.ini", # Windows folder customization
|
||||
".localized", # macOS folder name localization
|
||||
".gitkeep", # Placeholder to keep empty dirs in git
|
||||
".gitignore", # Git ignore rules
|
||||
})
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -180,6 +192,22 @@ def is_hash_folder(name: str) -> bool:
|
||||
return bool(_HEX_PATTERN.fullmatch(name or ""))
|
||||
|
||||
|
||||
def _is_safe_ignorable_entry(item: str, item_path: str) -> bool:
|
||||
"""Return True if *item* is a harmless system/hidden file we can skip.
|
||||
|
||||
These files are never created by the example images system and are safe to
|
||||
ignore because the cleanup/delete operations only act on **directories**,
|
||||
never on individual files (other than ``.download_progress.json``).
|
||||
"""
|
||||
if item in _SAFE_FILENAMES:
|
||||
return True
|
||||
# Hide Unix hidden files (dotfiles) that are regular files,
|
||||
# since the cleanup system never deletes or moves files.
|
||||
if item.startswith(".") and os.path.isfile(item_path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_example_images_root(folder_path: str) -> bool:
|
||||
"""Check whether a folder looks like a dedicated example images root."""
|
||||
|
||||
@@ -190,9 +218,16 @@ def is_valid_example_images_root(folder_path: str) -> bool:
|
||||
|
||||
for item in items:
|
||||
item_path = os.path.join(folder_path, item)
|
||||
|
||||
# .download_progress.json is an expected metadata file — check before
|
||||
# the generic dotfile rule so it stays explicitly documented.
|
||||
if item == ".download_progress.json" and os.path.isfile(item_path):
|
||||
continue
|
||||
|
||||
# Skip harmless system/hidden files — cleanup only touches directories
|
||||
if _is_safe_ignorable_entry(item, item_path):
|
||||
continue
|
||||
|
||||
if os.path.isdir(item_path):
|
||||
if is_hash_folder(item):
|
||||
continue
|
||||
@@ -211,6 +246,41 @@ def is_valid_example_images_root(folder_path: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def find_non_compliant_items_in_example_images_root(folder_path: str) -> list[str]:
|
||||
"""Return the names of items that prevent *folder_path* from being a valid
|
||||
example images root, or an empty list if the folder is valid.
|
||||
|
||||
This mirrors ``is_valid_example_images_root`` but **returns** the offending
|
||||
names instead of a boolean, so callers can produce actionable error messages.
|
||||
"""
|
||||
try:
|
||||
items = os.listdir(folder_path)
|
||||
except OSError as exc:
|
||||
return [f"<cannot list directory: {exc}>"]
|
||||
|
||||
offending: list[str] = []
|
||||
|
||||
for item in items:
|
||||
item_path = os.path.join(folder_path, item)
|
||||
|
||||
# Same skip rules as is_valid_example_images_root
|
||||
if item == ".download_progress.json" and os.path.isfile(item_path):
|
||||
continue
|
||||
if _is_safe_ignorable_entry(item, item_path):
|
||||
continue
|
||||
if os.path.isdir(item_path):
|
||||
if is_hash_folder(item):
|
||||
continue
|
||||
if item == "_deleted":
|
||||
continue
|
||||
if _library_folder_has_only_hash_dirs(item_path):
|
||||
continue
|
||||
|
||||
offending.append(item)
|
||||
|
||||
return offending
|
||||
|
||||
|
||||
def _library_folder_has_only_hash_dirs(path: str) -> bool:
|
||||
"""Return True when a library subfolder only contains hash folders or metadata files."""
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
from py.services.settings_manager import get_settings_manager
|
||||
from py.utils.example_images_paths import (
|
||||
ensure_library_root_exists,
|
||||
find_non_compliant_items_in_example_images_root,
|
||||
get_model_folder,
|
||||
get_model_relative_path,
|
||||
is_valid_example_images_root,
|
||||
@@ -140,3 +141,68 @@ def test_is_valid_example_images_root_accepts_legacy_library_structure(tmp_path,
|
||||
(hash_folder / 'image.png').write_text('data', encoding='utf-8')
|
||||
|
||||
assert is_valid_example_images_root(str(tmp_path)) is True
|
||||
|
||||
|
||||
def test_find_non_compliant_items_returns_empty_for_valid_root(tmp_path, settings_manager):
|
||||
"""An empty folder or one with only hash dirs should return []."""
|
||||
settings_manager.settings['example_images_path'] = str(tmp_path)
|
||||
|
||||
# Empty folder
|
||||
assert find_non_compliant_items_in_example_images_root(str(tmp_path)) == []
|
||||
|
||||
# Only hash folders
|
||||
hash_folder = tmp_path / ('f' * 64)
|
||||
hash_folder.mkdir()
|
||||
(hash_folder / 'image.png').write_text('data', encoding='utf-8')
|
||||
assert find_non_compliant_items_in_example_images_root(str(tmp_path)) == []
|
||||
|
||||
|
||||
def test_find_non_compliant_items_returns_offending_names(tmp_path, settings_manager):
|
||||
"""A folder with non-hash items should return their names."""
|
||||
settings_manager.settings['example_images_path'] = str(tmp_path)
|
||||
|
||||
# Create a valid hash folder so the root is otherwise acceptable
|
||||
hash_folder = tmp_path / ('a' * 64)
|
||||
hash_folder.mkdir()
|
||||
|
||||
# Add an offending file
|
||||
(tmp_path / 'readme.txt').write_text('hello', encoding='utf-8')
|
||||
assert find_non_compliant_items_in_example_images_root(str(tmp_path)) == ['readme.txt']
|
||||
|
||||
# Add an offending directory with content (empty dirs are accepted as
|
||||
# potential legacy library folders by _library_folder_has_only_hash_dirs)
|
||||
offending_dir = tmp_path / 'not_a_hash'
|
||||
offending_dir.mkdir()
|
||||
(offending_dir / 'some_file.txt').write_text('data', encoding='utf-8')
|
||||
items = find_non_compliant_items_in_example_images_root(str(tmp_path))
|
||||
assert 'readme.txt' in items
|
||||
assert 'not_a_hash' in items
|
||||
|
||||
|
||||
def test_find_non_compliant_items_ignores_hidden_files(tmp_path, settings_manager):
|
||||
"""Hidden/system files should not appear in offending list."""
|
||||
settings_manager.settings['example_images_path'] = str(tmp_path)
|
||||
|
||||
# .DS_Store is an allowed file
|
||||
(tmp_path / '.DS_Store').write_text('', encoding='utf-8')
|
||||
assert find_non_compliant_items_in_example_images_root(str(tmp_path)) == []
|
||||
|
||||
# Thumbs.db too
|
||||
(tmp_path / 'Thumbs.db').write_text('', encoding='utf-8')
|
||||
assert find_non_compliant_items_in_example_images_root(str(tmp_path)) == []
|
||||
|
||||
|
||||
def test_find_non_compliant_items_accepts_download_progress_json(tmp_path, settings_manager):
|
||||
""".download_progress.json should be recognised as a valid metadata file."""
|
||||
settings_manager.settings['example_images_path'] = str(tmp_path)
|
||||
|
||||
(tmp_path / '.download_progress.json').write_text('{}', encoding='utf-8')
|
||||
assert find_non_compliant_items_in_example_images_root(str(tmp_path)) == []
|
||||
|
||||
|
||||
def test_find_non_compliant_items_reports_directory_error(tmp_path):
|
||||
"""When the directory cannot be listed, return an explanatory message."""
|
||||
non_existent = tmp_path / 'does-not-exist'
|
||||
result = find_non_compliant_items_in_example_images_root(str(non_existent))
|
||||
assert len(result) == 1
|
||||
assert 'cannot list directory' in result[0]
|
||||
|
||||
Reference in New Issue
Block a user