From 139e9157117f8d1c5728119c73d67d60035b1fd0 Mon Sep 17 00:00:00 2001 From: pixelpaws Date: Mon, 27 Oct 2025 09:50:25 +0800 Subject: [PATCH] fix(recipes): normalize relocated preview paths --- py/services/recipe_scanner.py | 32 +++++++++++++++++---- py/services/recipes/persistence_service.py | 10 ++++--- tests/services/test_recipe_scanner.py | 33 ++++++++++++++++++++++ tests/services/test_recipe_services.py | 7 +++++ 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index f8a9aec8..ea27a924 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -384,16 +384,32 @@ class RecipeScanner: # Ensure the image file exists image_path = recipe_data.get('file_path') - if not os.path.exists(image_path): + normalized_image_path = os.path.normpath(image_path) if image_path else image_path + path_updated = False + if image_path and normalized_image_path != image_path: + recipe_data['file_path'] = normalized_image_path + image_path = normalized_image_path + path_updated = True + + if image_path and not os.path.exists(image_path): logger.warning(f"Recipe image not found: {image_path}") # Try to find the image in the same directory as the recipe recipe_dir = os.path.dirname(recipe_path) image_filename = os.path.basename(image_path) alternative_path = os.path.join(recipe_dir, image_filename) if os.path.exists(alternative_path): - recipe_data['file_path'] = alternative_path + normalized_alternative = os.path.normpath(alternative_path) + recipe_data['file_path'] = normalized_alternative + image_path = normalized_alternative + path_updated = True + logger.info( + "Updated recipe image path to %s after relocating asset", normalized_alternative + ) else: logger.warning(f"Could not find alternative image path for {image_path}") + + if path_updated: + self._write_recipe_file(recipe_path, recipe_data) # Ensure loras array exists if 'loras' not in recipe_data: @@ -413,18 +429,24 @@ class RecipeScanner: # Write updated recipe data back to file try: - with open(recipe_path, 'w', encoding='utf-8') as f: - json.dump(recipe_data, f, indent=4, ensure_ascii=False) + self._write_recipe_file(recipe_path, recipe_data) logger.info(f"Added fingerprint to recipe: {recipe_path}") except Exception as e: logger.error(f"Error writing updated recipe with fingerprint: {e}") - + return recipe_data except Exception as e: logger.error(f"Error loading recipe file {recipe_path}: {e}") import traceback traceback.print_exc(file=sys.stderr) return None + + @staticmethod + def _write_recipe_file(recipe_path: str, recipe_data: Dict[str, Any]) -> None: + """Persist ``recipe_data`` back to ``recipe_path`` with standard formatting.""" + + with open(recipe_path, 'w', encoding='utf-8') as file_obj: + json.dump(recipe_data, file_obj, indent=4, ensure_ascii=False) async def _update_lora_information(self, recipe_data: Dict) -> bool: """Update LoRA information with hash and file_name diff --git a/py/services/recipes/persistence_service.py b/py/services/recipes/persistence_service.py index 14b8632e..9f7896d4 100644 --- a/py/services/recipes/persistence_service.py +++ b/py/services/recipes/persistence_service.py @@ -73,7 +73,8 @@ class RecipePersistenceService: ) image_filename = f"{recipe_id}{extension}" image_path = os.path.join(recipes_dir, image_filename) - with open(image_path, "wb") as file_obj: + normalized_image_path = os.path.normpath(image_path) + with open(normalized_image_path, "wb") as file_obj: file_obj.write(optimized_image) current_time = time.time() @@ -97,7 +98,7 @@ class RecipePersistenceService: fingerprint = calculate_recipe_fingerprint(loras_data) recipe_data: Dict[str, Any] = { "id": recipe_id, - "file_path": image_path, + "file_path": normalized_image_path, "title": name, "modified": current_time, "created_date": current_time, @@ -116,10 +117,11 @@ class RecipePersistenceService: json_filename = f"{recipe_id}.recipe.json" json_path = os.path.join(recipes_dir, json_filename) + json_path = os.path.normpath(json_path) with open(json_path, "w", encoding="utf-8") as file_obj: json.dump(recipe_data, file_obj, indent=4, ensure_ascii=False) - self._exif_utils.append_recipe_metadata(image_path, recipe_data) + self._exif_utils.append_recipe_metadata(normalized_image_path, recipe_data) matching_recipes = await self._find_matching_recipes(recipe_scanner, fingerprint, exclude_id=recipe_id) await recipe_scanner.add_recipe(recipe_data) @@ -128,7 +130,7 @@ class RecipePersistenceService: { "success": True, "recipe_id": recipe_id, - "image_path": image_path, + "image_path": normalized_image_path, "json_path": json_path, "matching_recipes": matching_recipes, } diff --git a/tests/services/test_recipe_scanner.py b/tests/services/test_recipe_scanner.py index 63c18f25..20f25727 100644 --- a/tests/services/test_recipe_scanner.py +++ b/tests/services/test_recipe_scanner.py @@ -1,5 +1,6 @@ import asyncio import json +import os from pathlib import Path from types import SimpleNamespace @@ -183,3 +184,35 @@ async def test_update_lora_entry_updates_cache_and_file(tmp_path: Path, recipe_s cached_recipe = next(item for item in cache.raw_data if item["id"] == recipe_id) assert cached_recipe["loras"][0]["hash"] == target_hash assert cached_recipe["fingerprint"] == expected_fingerprint + + +@pytest.mark.asyncio +async def test_load_recipe_rewrites_missing_image_path(tmp_path: Path, recipe_scanner): + scanner, _ = recipe_scanner + recipes_dir = Path(config.loras_roots[0]) / "recipes" + recipes_dir.mkdir(parents=True, exist_ok=True) + + recipe_id = "moved" + old_root = tmp_path / "old_root" + old_path = old_root / "recipes" / f"{recipe_id}.webp" + recipe_path = recipes_dir / f"{recipe_id}.recipe.json" + current_image = recipes_dir / f"{recipe_id}.webp" + current_image.write_bytes(b"image-bytes") + + recipe_data = { + "id": recipe_id, + "file_path": str(old_path), + "title": "Relocated", + "modified": 0.0, + "created_date": 0.0, + "loras": [], + } + recipe_path.write_text(json.dumps(recipe_data)) + + loaded = await scanner._load_recipe_file(str(recipe_path)) + + expected_path = os.path.normpath(str(current_image)) + assert loaded["file_path"] == expected_path + + persisted = json.loads(recipe_path.read_text()) + assert persisted["file_path"] == expected_path diff --git a/tests/services/test_recipe_services.py b/tests/services/test_recipe_services.py index 81a15424..1b1f5066 100644 --- a/tests/services/test_recipe_services.py +++ b/tests/services/test_recipe_services.py @@ -1,5 +1,7 @@ +import json import logging import os +from pathlib import Path from types import SimpleNamespace import pytest @@ -148,3 +150,8 @@ async def test_save_recipe_reports_duplicates(tmp_path): assert scanner.last_fingerprint is not None assert os.path.exists(result.payload["json_path"]) assert scanner._cache.raw_data + + stored = json.loads(Path(result.payload["json_path"]).read_text()) + expected_image_path = os.path.normpath(result.payload["image_path"]) + assert stored["file_path"] == expected_image_path + assert service._exif_utils.appended[0] == expected_image_path