fix(sidebar): keep the recipes storage dir out of folder management

The default recipes dir (<first lora root>/recipes) is auto-created by the
recipe scanner and recorded by the model-root walks as an empty folder, so
it showed up in the folder sidebar — and since recipe files are not model
weight files, the folder delete endpoint's model_count check let a
right-click delete wipe the whole recipe library via shutil.rmtree.

- scan walks (full scan, reconcile, all-folders backfill) no longer descend
  into the effective recipes dir, and get_all_folders() filters it out of
  snapshots persisted before the exclusion existed
- delete_folder/rename_folder refuse the recipes dir and any ancestor of
  it with code "protected"
- the delete modal renders a protected state (confirm hidden, per-row
  checkbox disabled) instead of a deletable 'no models' prediction
- RecipeScanner.recipes_dir now shares the side-effect-free resolver in
  py/utils/recipes_paths.py
This commit is contained in:
Will Miao
2026-10-07 19:38:28 +08:00
parent f4d0228d6b
commit 1805519f4f
9 changed files with 397 additions and 26 deletions
+26
View File
@@ -8,6 +8,7 @@ from abc import ABC, abstractmethod
from ..utils.utils import calculate_relative_path_for_model, remove_empty_dirs
from ..utils.constants import AUTO_ORGANIZE_BATCH_SIZE, MODEL_FILE_EXTENSIONS
from ..utils.recipes_paths import path_is_or_contains_recipes_dir
from ..utils.sidecar_paths import is_centralized, resolve_centralized_dir_for_dir
from ..services.settings_manager import get_settings_manager
from ..services.model_lifecycle_service import _require_path_in_library_roots
@@ -697,6 +698,19 @@ class ModelMoveService:
"error": "The library root itself cannot be deleted",
}
if path_is_or_contains_recipes_dir(absolute_path):
# The recipes dir holds no model weight files, so the
# model_count check below cannot protect it — but removing it
# wipes the user's recipe library.
return {
"success": False,
"code": "protected",
"error": (
"This folder holds the recipe library and cannot be "
"deleted from here"
),
}
manifest = self._collect_folder_manifest(absolute_path)
if manifest["pending_delete_job"]:
@@ -948,6 +962,18 @@ class ModelMoveService:
"error": "The library root itself cannot be renamed",
}
if path_is_or_contains_recipes_dir(absolute_path):
# Renaming the recipes dir (or an ancestor) strands the recipe
# library: the scanner keeps looking at the configured path.
return {
"success": False,
"code": "protected",
"error": (
"This folder holds the recipe library and cannot be "
"renamed from here"
),
}
previous_relative = self._calculate_relative_folder(absolute_path)
target = os.path.join(os.path.dirname(absolute_path), new_name)