mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-22 11:34:08 -03:00
fix(recipes): keep source_path empty on local-fallback re-import
Re-importing a file-imported recipe fell back to its own saved preview image, then recorded that internal path as the new recipe's source_path. Since the old preview is deleted with the old recipe, this left a dangling source_path that showed up as a bogus source URL and blocked any further re-import with 'no re-importable source'. Only persist source_path when the re-import source is an accessible external file; otherwise keep it empty. Also let a dangling non-URL source_path fall back to the recipe's own image so existing affected recipes can re-import again.
This commit is contained in:
@@ -1128,15 +1128,21 @@ class RecipeManagementHandler:
|
|||||||
image_id = extract_civitai_image_id(source_path) if source_path else None
|
image_id = extract_civitai_image_id(source_path) if source_path else None
|
||||||
|
|
||||||
# Local re-import sources: an explicit local source_path, or — when
|
# Local re-import sources: an explicit local source_path, or — when
|
||||||
# no source_path was recorded (drag & drop / file-picker imports) —
|
# no usable source_path was recorded (drag & drop / file-picker
|
||||||
# the recipe's own saved image, which still carries the original
|
# imports, or a dangling path left by an earlier re-import) — the
|
||||||
|
# recipe's own saved image, which still carries the original
|
||||||
# embedded generation metadata next to the recipe metadata block.
|
# embedded generation metadata next to the recipe metadata block.
|
||||||
|
# In the fallback case nothing is persisted as source_path: the
|
||||||
|
# recipe's own previous preview is not an external source, and it
|
||||||
|
# is deleted together with the old recipe below.
|
||||||
local_source = None
|
local_source = None
|
||||||
|
persisted_source_path = ""
|
||||||
if not image_id and source_path and os.path.isfile(source_path):
|
if not image_id and source_path and os.path.isfile(source_path):
|
||||||
local_source = source_path
|
local_source = source_path
|
||||||
|
persisted_source_path = source_path
|
||||||
elif (
|
elif (
|
||||||
not image_id
|
not image_id
|
||||||
and not source_path
|
and not source_path.startswith(("http://", "https://"))
|
||||||
and old_file_path
|
and old_file_path
|
||||||
and os.path.isfile(old_file_path)
|
and os.path.isfile(old_file_path)
|
||||||
):
|
):
|
||||||
@@ -1170,6 +1176,7 @@ class RecipeManagementHandler:
|
|||||||
target_dir=old_folder,
|
target_dir=old_folder,
|
||||||
user_edits=user_edits,
|
user_edits=user_edits,
|
||||||
old_title=old_recipe.get("title", ""),
|
old_title=old_recipe.get("title", ""),
|
||||||
|
persisted_source_path=persisted_source_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with self._import_semaphore:
|
async with self._import_semaphore:
|
||||||
@@ -2512,6 +2519,7 @@ class RecipeManagementHandler:
|
|||||||
target_dir: str | None,
|
target_dir: str | None,
|
||||||
user_edits: dict[str, Any],
|
user_edits: dict[str, Any],
|
||||||
old_title: str,
|
old_title: str,
|
||||||
|
persisted_source_path: str,
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
"""Re-import a recipe from a local image file.
|
"""Re-import a recipe from a local image file.
|
||||||
|
|
||||||
@@ -2519,6 +2527,12 @@ class RecipeManagementHandler:
|
|||||||
generation metadata (the appended recipe metadata block is ignored so
|
generation metadata (the appended recipe metadata block is ignored so
|
||||||
the current parser gets a fresh pass), saves a new recipe, then deletes
|
the current parser gets a fresh pass), saves a new recipe, then deletes
|
||||||
the old one.
|
the old one.
|
||||||
|
|
||||||
|
``persisted_source_path`` is the source_path recorded on the new
|
||||||
|
recipe: the external source file when one exists, or empty when the
|
||||||
|
re-import fell back to the recipe's own previous preview image (that
|
||||||
|
file is deleted with the old recipe, so recording it would leave a
|
||||||
|
dangling path that blocks future re-imports).
|
||||||
"""
|
"""
|
||||||
normalized = os.path.normpath(file_path)
|
normalized = os.path.normpath(file_path)
|
||||||
if not os.path.isfile(normalized):
|
if not os.path.isfile(normalized):
|
||||||
@@ -2547,7 +2561,7 @@ class RecipeManagementHandler:
|
|||||||
"base_model": base_model,
|
"base_model": base_model,
|
||||||
"loras": loras,
|
"loras": loras,
|
||||||
"gen_params": gen_params,
|
"gen_params": gen_params,
|
||||||
"source_path": normalized,
|
"source_path": persisted_source_path,
|
||||||
}
|
}
|
||||||
if checkpoint:
|
if checkpoint:
|
||||||
metadata["checkpoint"] = checkpoint
|
metadata["checkpoint"] = checkpoint
|
||||||
@@ -2610,7 +2624,7 @@ class RecipeManagementHandler:
|
|||||||
"success": True,
|
"success": True,
|
||||||
"old_recipe_id": recipe_id,
|
"old_recipe_id": recipe_id,
|
||||||
"recipe_id": new_recipe_id,
|
"recipe_id": new_recipe_id,
|
||||||
"source_path": normalized,
|
"source_path": persisted_source_path,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2222,12 +2222,94 @@ async def test_reimport_without_source_path_falls_back_to_recipe_file(
|
|||||||
# The already-optimized preview image must be stored verbatim.
|
# The already-optimized preview image must be stored verbatim.
|
||||||
assert harness.persistence.save_calls[-1]["skip_optimize"] is True
|
assert harness.persistence.save_calls[-1]["skip_optimize"] is True
|
||||||
assert harness.persistence.save_calls[-1]["image_bytes"] == b"fake-image"
|
assert harness.persistence.save_calls[-1]["image_bytes"] == b"fake-image"
|
||||||
|
# The fallback source is the recipe's own previous preview, which gets
|
||||||
|
# deleted with the old recipe — it must not be recorded as source_path.
|
||||||
|
assert harness.persistence.save_calls[-1]["metadata"]["source_path"] == ""
|
||||||
# User edits (title, tags) are carried over to the new recipe.
|
# User edits (title, tags) are carried over to the new recipe.
|
||||||
assert harness.persistence.update_calls[-1]["recipe_id"] == "new-rec"
|
assert harness.persistence.update_calls[-1]["recipe_id"] == "new-rec"
|
||||||
assert harness.persistence.update_calls[-1]["updates"]["title"] == "Old title"
|
assert harness.persistence.update_calls[-1]["updates"]["title"] == "Old title"
|
||||||
assert harness.persistence.update_calls[-1]["updates"]["tags"] == ["tag1"]
|
assert harness.persistence.update_calls[-1]["updates"]["tags"] == ["tag1"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reimport_with_dangling_source_path_falls_back_to_recipe_file(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""A source_path pointing to a deleted file (left by an earlier re-import)
|
||||||
|
must not block re-import: fall back to the recipe's own saved image and
|
||||||
|
clear the dangling source_path."""
|
||||||
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
|
recipe_file = harness.tmp_dir / "recipes" / "rec3.webp"
|
||||||
|
recipe_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
recipe_file.write_bytes(b"fake-image")
|
||||||
|
|
||||||
|
harness.scanner.recipes["rec3"] = {
|
||||||
|
"id": "rec3",
|
||||||
|
"title": "Dangling source",
|
||||||
|
"file_path": str(recipe_file),
|
||||||
|
"tags": [],
|
||||||
|
# Dangling local path: the file no longer exists.
|
||||||
|
"source_path": str(harness.tmp_dir / "recipes" / "deleted.webp"),
|
||||||
|
}
|
||||||
|
harness.analysis.result = SimpleNamespace(
|
||||||
|
payload={"success": True, "recipe_id": "new-rec-3", "loras": []},
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
harness.persistence.save_result = SimpleNamespace(
|
||||||
|
payload={"success": True, "recipe_id": "new-rec-3"}, status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await harness.client.post("/api/lm/recipe/rec3/reimport")
|
||||||
|
payload = await response.json()
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["recipe_id"] == "new-rec-3"
|
||||||
|
assert harness.analysis.local_calls == [str(recipe_file)]
|
||||||
|
assert harness.persistence.delete_calls == ["rec3"]
|
||||||
|
# The dangling path is not carried over to the new recipe.
|
||||||
|
assert harness.persistence.save_calls[-1]["metadata"]["source_path"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reimport_with_accessible_local_source_keeps_source_path(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""When the recorded source_path is an existing external file, it remains
|
||||||
|
the source of truth and stays recorded on the new recipe."""
|
||||||
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
|
source_file = harness.tmp_dir / "imports" / "original.png"
|
||||||
|
source_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
source_file.write_bytes(b"original-image")
|
||||||
|
recipe_file = harness.tmp_dir / "recipes" / "rec4.webp"
|
||||||
|
recipe_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
recipe_file.write_bytes(b"fake-image")
|
||||||
|
|
||||||
|
harness.scanner.recipes["rec4"] = {
|
||||||
|
"id": "rec4",
|
||||||
|
"title": "External source",
|
||||||
|
"file_path": str(recipe_file),
|
||||||
|
"tags": [],
|
||||||
|
"source_path": str(source_file),
|
||||||
|
}
|
||||||
|
harness.analysis.result = SimpleNamespace(
|
||||||
|
payload={"success": True, "recipe_id": "new-rec-4", "loras": []},
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
harness.persistence.save_result = SimpleNamespace(
|
||||||
|
payload={"success": True, "recipe_id": "new-rec-4"}, status=200
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await harness.client.post("/api/lm/recipe/rec4/reimport")
|
||||||
|
payload = await response.json()
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
# The external source file is re-parsed, not the recipe preview.
|
||||||
|
assert harness.analysis.local_calls == [str(source_file)]
|
||||||
|
assert harness.persistence.save_calls[-1]["metadata"]["source_path"] == str(
|
||||||
|
source_file
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_reimport_without_any_source_returns_400(
|
async def test_reimport_without_any_source_returns_400(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user