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,
|
VALID_LORA_TYPES,
|
||||||
)
|
)
|
||||||
from ...utils.civitai_utils import rewrite_preview_url
|
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.lora_metadata import extract_trained_words
|
||||||
from ...utils.session_logging import get_standalone_session_log_snapshot
|
from ...utils.session_logging import get_standalone_session_log_snapshot
|
||||||
from ...utils.usage_stats import UsageStats
|
from ...utils.usage_stats import UsageStats
|
||||||
@@ -1498,6 +1501,16 @@ class SettingsHandler:
|
|||||||
if not os.path.isdir(folder_path):
|
if not os.path.isdir(folder_path):
|
||||||
return "Please set a dedicated folder for example images."
|
return "Please set a dedicated folder for example images."
|
||||||
if not self._is_dedicated_example_images_folder(folder_path):
|
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 "Please set a dedicated folder for example images."
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ from ..services.settings_manager import get_settings_manager
|
|||||||
|
|
||||||
_HEX_PATTERN = re.compile(r"[a-fA-F0-9]{64}")
|
_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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -180,6 +192,22 @@ def is_hash_folder(name: str) -> bool:
|
|||||||
return bool(_HEX_PATTERN.fullmatch(name or ""))
|
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:
|
def is_valid_example_images_root(folder_path: str) -> bool:
|
||||||
"""Check whether a folder looks like a dedicated example images root."""
|
"""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:
|
for item in items:
|
||||||
item_path = os.path.join(folder_path, item)
|
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):
|
if item == ".download_progress.json" and os.path.isfile(item_path):
|
||||||
continue
|
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 os.path.isdir(item_path):
|
||||||
if is_hash_folder(item):
|
if is_hash_folder(item):
|
||||||
continue
|
continue
|
||||||
@@ -211,6 +246,41 @@ def is_valid_example_images_root(folder_path: str) -> bool:
|
|||||||
return True
|
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:
|
def _library_folder_has_only_hash_dirs(path: str) -> bool:
|
||||||
"""Return True when a library subfolder only contains hash folders or metadata files."""
|
"""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.services.settings_manager import get_settings_manager
|
||||||
from py.utils.example_images_paths import (
|
from py.utils.example_images_paths import (
|
||||||
ensure_library_root_exists,
|
ensure_library_root_exists,
|
||||||
|
find_non_compliant_items_in_example_images_root,
|
||||||
get_model_folder,
|
get_model_folder,
|
||||||
get_model_relative_path,
|
get_model_relative_path,
|
||||||
is_valid_example_images_root,
|
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')
|
(hash_folder / 'image.png').write_text('data', encoding='utf-8')
|
||||||
|
|
||||||
assert is_valid_example_images_root(str(tmp_path)) is True
|
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