diff --git a/py/api/__init__.py b/py/api/__init__.py index 457565d..723c78c 100644 --- a/py/api/__init__.py +++ b/py/api/__init__.py @@ -448,8 +448,15 @@ class PromptManagerAPI( def _find_comfyui_output_dir(self): """Locate the ComfyUI output directory using multiple detection strategies. - Results are cached after first successful lookup since the output - directory does not change during runtime. + Checks in order: + 1. User-configured directory (GalleryConfig.MONITORING_DIRECTORIES) + 2. Upward filesystem search for ComfyUI markers + 3. Common installation path patterns + 4. Common ComfyUI installation locations + + Results are cached after first successful lookup. The cache is + invalidated when the user changes the configured directory via + save_settings(). Returns: str or None: Absolute path to ComfyUI output directory, or None @@ -458,6 +465,27 @@ class PromptManagerAPI( if self._cached_output_dir is not None: return self._cached_output_dir + # Method 0: Check user-configured directory first + try: + from ..config import GalleryConfig + + if GalleryConfig.MONITORING_DIRECTORIES: + configured_dir = Path(GalleryConfig.MONITORING_DIRECTORIES[0]).resolve() + if configured_dir.exists() and configured_dir.is_dir(): + self.logger.info( + f"Using configured monitoring directory: {configured_dir}" + ) + self._cached_output_dir = str(configured_dir) + return self._cached_output_dir + else: + self.logger.warning( + f"Configured directory does not exist: {GalleryConfig.MONITORING_DIRECTORIES[0]}" + ) + except ImportError: + self.logger.debug( + "GalleryConfig not available, skipping configured directory check" + ) + current_file = Path(__file__).resolve() self.logger.debug(f"Starting ComfyUI output search from: {current_file}") diff --git a/py/api/admin.py b/py/api/admin.py index 2e0db4e..b4060c6 100644 --- a/py/api/admin.py +++ b/py/api/admin.py @@ -448,6 +448,10 @@ class AdminRoutesMixin: GalleryConfig.MONITORING_DIRECTORIES = [new_path] else: GalleryConfig.MONITORING_DIRECTORIES = [] + # Invalidate caches so next lookup uses new config + self._cached_output_dir = None + self._gallery_cache = None + self._gallery_cache_time = 0 restart_required = True # Save to config file for persistence diff --git a/pyproject.toml b/pyproject.toml index c00407e..75cbf53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "promptmanager" description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite." -version = "3.0.36" +version = "3.0.37" license = {file = "LICENSE"} dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"] diff --git a/tests/test_output_dir.py b/tests/test_output_dir.py new file mode 100644 index 0000000..4920690 --- /dev/null +++ b/tests/test_output_dir.py @@ -0,0 +1,115 @@ +""" +Tests for _find_comfyui_output_dir() — verifies that user-configured +directories take priority over auto-detection, and that the cache is +invalidated when the setting changes. +""" + +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Mock ComfyUI's server module before importing anything that touches config +_mock_server = MagicMock() +_mock_server.PromptServer.instance.routes = MagicMock() +sys.modules["server"] = _mock_server + +from py.api import PromptManagerAPI +from py.config import GalleryConfig + + +class TestFindComfyuiOutputDir(unittest.TestCase): + """Test _find_comfyui_output_dir() respects user-configured directories.""" + + def setUp(self): + self.api = PromptManagerAPI() + self.api._cached_output_dir = None + self._orig_dirs = list(GalleryConfig.MONITORING_DIRECTORIES) + + def tearDown(self): + GalleryConfig.MONITORING_DIRECTORIES = self._orig_dirs + + def test_configured_directory_takes_priority(self): + """When a valid directory is configured, it should be used instead of auto-detect.""" + with tempfile.TemporaryDirectory() as tmpdir: + GalleryConfig.MONITORING_DIRECTORIES = [tmpdir] + self.api._cached_output_dir = None + + result = self.api._find_comfyui_output_dir() + + self.assertEqual(result, str(Path(tmpdir).resolve())) + + def test_nonexistent_configured_directory_falls_through(self): + """When the configured directory doesn't exist, fall back to auto-detection.""" + GalleryConfig.MONITORING_DIRECTORIES = ["/nonexistent/fake/path/output"] + self.api._cached_output_dir = None + + # Should not return the nonexistent path — will either find + # ComfyUI output via auto-detect or return None + result = self.api._find_comfyui_output_dir() + + self.assertNotEqual(result, "/nonexistent/fake/path/output") + + def test_empty_config_uses_auto_detection(self): + """When MONITORING_DIRECTORIES is empty, auto-detection should be used.""" + GalleryConfig.MONITORING_DIRECTORIES = [] + self.api._cached_output_dir = None + + # Should not raise — auto-detection may or may not find a dir + result = self.api._find_comfyui_output_dir() + # Result is either a valid path or None + if result is not None: + self.assertTrue(Path(result).is_dir()) + + def test_cache_returns_same_value(self): + """After first lookup, cached value should be returned.""" + with tempfile.TemporaryDirectory() as tmpdir: + GalleryConfig.MONITORING_DIRECTORIES = [tmpdir] + self.api._cached_output_dir = None + + result1 = self.api._find_comfyui_output_dir() + # Change config — should NOT affect result because of cache + GalleryConfig.MONITORING_DIRECTORIES = ["/some/other/path"] + result2 = self.api._find_comfyui_output_dir() + + self.assertEqual(result1, result2) + + def test_cache_invalidation_picks_up_new_directory(self): + """After clearing the cache, _find_comfyui_output_dir should use new config.""" + with tempfile.TemporaryDirectory() as tmpdir1: + with tempfile.TemporaryDirectory() as tmpdir2: + GalleryConfig.MONITORING_DIRECTORIES = [tmpdir1] + self.api._cached_output_dir = None + + result1 = self.api._find_comfyui_output_dir() + self.assertEqual(result1, str(Path(tmpdir1).resolve())) + + # Simulate what save_settings does: update config + invalidate cache + GalleryConfig.MONITORING_DIRECTORIES = [tmpdir2] + self.api._cached_output_dir = None + + result2 = self.api._find_comfyui_output_dir() + self.assertEqual(result2, str(Path(tmpdir2).resolve())) + + def test_configured_directory_is_resolved(self): + """Configured paths with symlinks or .. should be resolved to absolute.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Use a relative-ish path with .. + parent = str(Path(tmpdir).parent) + basename = Path(tmpdir).name + dotdot_path = os.path.join(parent, ".", basename) + + GalleryConfig.MONITORING_DIRECTORIES = [dotdot_path] + self.api._cached_output_dir = None + + result = self.api._find_comfyui_output_dir() + + self.assertEqual(result, str(Path(tmpdir).resolve())) + + +if __name__ == "__main__": + unittest.main()