feat(settings): migrate settings to user config dir

This commit is contained in:
pixelpaws
2025-09-27 22:22:15 +08:00
parent 1684978693
commit 88d5caf642
9 changed files with 146 additions and 16 deletions

View File

@@ -140,8 +140,11 @@ Enhance your Civitai browsing experience with our companion browser extension! S
### Option 2: **Portable Standalone Edition** (No ComfyUI required) ### Option 2: **Portable Standalone Edition** (No ComfyUI required)
1. Download the [Portable Package](https://github.com/willmiao/ComfyUI-Lora-Manager/releases/download/v0.9.2/lora_manager_portable.7z) 1. Download the [Portable Package](https://github.com/willmiao/ComfyUI-Lora-Manager/releases/download/v0.9.2/lora_manager_portable.7z)
2. Copy the provided `settings.json.example` file to create a new file named `settings.json` in `comfyui-lora-manager` folder 2. Copy the provided `settings.json.example` file to your LoRA Manager settings folder and rename it to `settings.json`:
3. Edit `settings.json` to include your correct model folder paths and CivitAI API key - **Windows:** `%APPDATA%/ComfyUI-LoRA-Manager/settings.json`
- **macOS:** `~/Library/Application Support/ComfyUI-LoRA-Manager/settings.json`
- **Linux:** `${XDG_CONFIG_HOME:-~/.config}/ComfyUI-LoRA-Manager/settings.json`
3. Edit the new `settings.json` to include your correct model folder paths and CivitAI API key
4. Run run.bat 4. Run run.bat
- To change the startup port, edit `run.bat` and modify the parameter (e.g. `--port 9001`) - To change the startup port, edit `run.bat` and modify the parameter (e.g. `--port 9001`)
@@ -209,7 +212,7 @@ You can combine multiple patterns to create detailed, organized filenames for yo
You can now run LoRA Manager independently from ComfyUI: You can now run LoRA Manager independently from ComfyUI:
1. **For ComfyUI users**: 1. **For ComfyUI users**:
- Launch ComfyUI with LoRA Manager at least once to initialize the necessary path information in the `settings.json` file. - Launch ComfyUI with LoRA Manager at least once to initialize the necessary path information in the `settings.json` file located in your user settings folder (see paths above).
- Make sure dependencies are installed: `pip install -r requirements.txt` - Make sure dependencies are installed: `pip install -r requirements.txt`
- From your ComfyUI root directory, run: - From your ComfyUI root directory, run:
```bash ```bash
@@ -222,7 +225,7 @@ You can now run LoRA Manager independently from ComfyUI:
``` ```
2. **For non-ComfyUI users**: 2. **For non-ComfyUI users**:
- Copy the provided `settings.json.example` file to create a new file named `settings.json` - Copy the provided `settings.json.example` file to the LoRA Manager settings folder (`%APPDATA%/ComfyUI-LoRA-Manager/`, `~/Library/Application Support/ComfyUI-LoRA-Manager/`, or `${XDG_CONFIG_HOME:-~/.config}/ComfyUI-LoRA-Manager/`) and rename it to `settings.json`
- Edit `settings.json` to include your correct model folder paths and CivitAI API key - Edit `settings.json` to include your correct model folder paths and CivitAI API key
- Install required dependencies: `pip install -r requirements.txt` - Install required dependencies: `pip install -r requirements.txt`
- Run standalone mode: - Run standalone mode:
@@ -231,6 +234,8 @@ You can now run LoRA Manager independently from ComfyUI:
``` ```
- Access the interface through your browser at: `http://localhost:8188/loras` - Access the interface through your browser at: `http://localhost:8188/loras`
> **Note:** Existing installations automatically migrate the legacy `settings.json` from the plugin folder to the user settings directory the first time you launch this version.
This standalone mode provides a lightweight option for managing your model and recipe collection without needing to run the full ComfyUI environment, making it useful even for users who primarily use other stable diffusion interfaces. This standalone mode provides a lightweight option for managing your model and recipe collection without needing to run the full ComfyUI environment, making it useful even for users who primarily use other stable diffusion interfaces.
## Testing & Coverage ## Testing & Coverage

View File

@@ -6,6 +6,8 @@ import logging
import json import json
import urllib.parse import urllib.parse
from py.utils.settings_paths import ensure_settings_file
# Use an environment variable to control standalone mode # Use an environment variable to control standalone mode
standalone_mode = os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0" standalone_mode = os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0"
@@ -40,12 +42,12 @@ class Config:
try: try:
# Check if we're running in ComfyUI mode (not standalone) # Check if we're running in ComfyUI mode (not standalone)
# Load existing settings # Load existing settings
settings_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'settings.json') settings_path = ensure_settings_file(logger)
settings = {} settings = {}
if os.path.exists(settings_path): if os.path.exists(settings_path):
with open(settings_path, 'r', encoding='utf-8') as f: with open(settings_path, 'r', encoding='utf-8') as f:
settings = json.load(f) settings = json.load(f)
# Update settings with paths # Update settings with paths
settings['folder_paths'] = { settings['folder_paths'] = {
'loras': self.loras_roots, 'loras': self.loras_roots,

View File

@@ -8,6 +8,8 @@ import tempfile
import asyncio import asyncio
from aiohttp import web, ClientError from aiohttp import web, ClientError
from typing import Dict, List from typing import Dict, List
from py.utils.settings_paths import ensure_settings_file
from ..services.downloader import get_downloader from ..services.downloader import get_downloader
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -121,7 +123,7 @@ class UpdateRoutes:
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
plugin_root = os.path.dirname(os.path.dirname(current_dir)) plugin_root = os.path.dirname(os.path.dirname(current_dir))
settings_path = os.path.join(plugin_root, 'settings.json') settings_path = ensure_settings_file(logger)
settings_backup = None settings_backup = None
if os.path.exists(settings_path): if os.path.exists(settings_path):
with open(settings_path, 'r', encoding='utf-8') as f: with open(settings_path, 'r', encoding='utf-8') as f:

View File

@@ -1,8 +1,10 @@
import os
import json import json
import os
import logging import logging
from typing import Any, Dict from typing import Any, Dict
from py.utils.settings_paths import ensure_settings_file
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,7 +38,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
class SettingsManager: class SettingsManager:
def __init__(self): def __init__(self):
self.settings_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'settings.json') self.settings_file = ensure_settings_file(logger)
self.settings = self._load_settings() self.settings = self._load_settings()
self._migrate_setting_keys() self._migrate_setting_keys()
self._ensure_default_settings() self._ensure_default_settings()

View File

@@ -0,0 +1,84 @@
"""Utilities for locating and migrating the LoRA Manager settings file."""
from __future__ import annotations
import logging
import os
import shutil
from typing import Optional
from platformdirs import user_config_dir
APP_NAME = "ComfyUI-LoRA-Manager"
_LOGGER = logging.getLogger(__name__)
def get_project_root() -> str:
"""Return the root directory of the project repository."""
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def get_legacy_settings_path() -> str:
"""Return the legacy location of ``settings.json`` within the project tree."""
return os.path.join(get_project_root(), "settings.json")
def get_settings_dir(create: bool = True) -> str:
"""Return the user configuration directory for the application.
Args:
create: Whether to create the directory if it does not already exist.
Returns:
The absolute path to the user configuration directory.
"""
config_dir = user_config_dir(APP_NAME, appauthor=False)
if create:
os.makedirs(config_dir, exist_ok=True)
return config_dir
def get_settings_file_path(create_dir: bool = True) -> str:
"""Return the path to ``settings.json`` in the user configuration directory."""
return os.path.join(get_settings_dir(create=create_dir), "settings.json")
def ensure_settings_file(logger: Optional[logging.Logger] = None) -> str:
"""Ensure the settings file resides in the user configuration directory.
If a legacy ``settings.json`` is detected in the project root it is migrated to
the platform-specific user configuration folder. The caller receives the path
to the settings file irrespective of whether a migration was needed.
Args:
logger: Optional logger used for migration messages. Falls back to a
module level logger when omitted.
Returns:
The absolute path to ``settings.json`` in the user configuration folder.
"""
logger = logger or _LOGGER
target_path = get_settings_file_path(create_dir=True)
legacy_path = get_legacy_settings_path()
if os.path.exists(legacy_path) and not os.path.exists(target_path):
try:
os.makedirs(os.path.dirname(target_path), exist_ok=True)
shutil.move(legacy_path, target_path)
logger.info("Migrated settings.json to %s", target_path)
except Exception as exc: # pragma: no cover - defensive fallback path
logger.warning("Failed to move legacy settings.json: %s", exc)
try:
shutil.copy2(legacy_path, target_path)
logger.info("Copied legacy settings.json to %s", target_path)
except Exception as copy_exc: # pragma: no cover - defensive fallback path
logger.error("Could not migrate settings.json: %s", copy_exc)
return target_path

View File

@@ -13,7 +13,8 @@ dependencies = [
"toml", "toml",
"natsort", "natsort",
"GitPython", "GitPython",
"aiosqlite" "aiosqlite",
"platformdirs"
] ]
[project.urls] [project.urls]

View File

@@ -10,3 +10,4 @@ natsort
GitPython GitPython
aiosqlite aiosqlite
beautifulsoup4 beautifulsoup4
platformdirs

View File

@@ -3,6 +3,7 @@ import os
import sys import sys
import json import json
from py.middleware.cache_middleware import cache_control from py.middleware.cache_middleware import cache_control
from py.utils.settings_paths import ensure_settings_file, get_settings_dir
# Set environment variable to indicate standalone mode # Set environment variable to indicate standalone mode
os.environ["COMFYUI_LORA_MANAGER_STANDALONE"] = "1" os.environ["COMFYUI_LORA_MANAGER_STANDALONE"] = "1"
@@ -32,7 +33,7 @@ class MockFolderPaths:
@staticmethod @staticmethod
def get_folder_paths(folder_name): def get_folder_paths(folder_name):
# Load paths from settings.json # Load paths from settings.json
settings_path = os.path.join(os.path.dirname(__file__), 'settings.json') settings_path = ensure_settings_file()
try: try:
if os.path.exists(settings_path): if os.path.exists(settings_path):
with open(settings_path, 'r', encoding='utf-8') as f: with open(settings_path, 'r', encoding='utf-8') as f:
@@ -159,7 +160,7 @@ class StandaloneServer:
self.app.router.add_get('/', self.handle_status) self.app.router.add_get('/', self.handle_status)
# Add static route for example images if the path exists in settings # Add static route for example images if the path exists in settings
settings_path = os.path.join(os.path.dirname(__file__), 'settings.json') settings_path = ensure_settings_file(logger)
if os.path.exists(settings_path): if os.path.exists(settings_path):
with open(settings_path, 'r', encoding='utf-8') as f: with open(settings_path, 'r', encoding='utf-8') as f:
settings = json.load(f) settings = json.load(f)
@@ -219,16 +220,19 @@ from py.lora_manager import LoraManager
def validate_settings(): def validate_settings():
"""Validate that settings.json exists and has required configuration""" """Validate that settings.json exists and has required configuration"""
settings_path = os.path.join(os.path.dirname(__file__), 'settings.json') settings_path = ensure_settings_file(logger)
if not os.path.exists(settings_path): if not os.path.exists(settings_path):
logger.error("=" * 80) logger.error("=" * 80)
logger.error("CONFIGURATION ERROR: settings.json file not found!") logger.error("CONFIGURATION ERROR: settings.json file not found!")
logger.error("") logger.error("")
logger.error("Expected location: %s", settings_path)
logger.error("")
logger.error("To run in standalone mode, you need to create a settings.json file.") logger.error("To run in standalone mode, you need to create a settings.json file.")
logger.error("Please follow these steps:") logger.error("Please follow these steps:")
logger.error("") logger.error("")
logger.error("1. Copy the provided settings.json.example file to create a new file") logger.error("1. Copy the provided settings.json.example file to create a new file")
logger.error(" named settings.json in the comfyui-lora-manager folder") logger.error(" named settings.json inside the LoRA Manager settings folder:")
logger.error(" %s", get_settings_dir())
logger.error("") logger.error("")
logger.error("2. Edit settings.json to include your correct model folder paths") logger.error("2. Edit settings.json to include your correct model folder paths")
logger.error(" and CivitAI API key") logger.error(" and CivitAI API key")

View File

@@ -3,21 +3,32 @@ import json
import pytest import pytest
from py.services.settings_manager import SettingsManager from py.services.settings_manager import SettingsManager
from py.utils import settings_paths
@pytest.fixture @pytest.fixture
def manager(tmp_path, monkeypatch): def manager(tmp_path, monkeypatch):
monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None) monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None)
fake_settings_path = tmp_path / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(fake_settings_path),
)
mgr = SettingsManager() mgr = SettingsManager()
mgr.settings_file = str(tmp_path / "settings.json") mgr.settings_file = str(fake_settings_path)
return mgr return mgr
def test_environment_variable_overrides_settings(tmp_path, monkeypatch): def test_environment_variable_overrides_settings(tmp_path, monkeypatch):
monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None) monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None)
monkeypatch.setenv("CIVITAI_API_KEY", "secret") monkeypatch.setenv("CIVITAI_API_KEY", "secret")
fake_settings_path = tmp_path / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(fake_settings_path),
)
mgr = SettingsManager() mgr = SettingsManager()
mgr.settings_file = str(tmp_path / "settings.json") mgr.settings_file = str(fake_settings_path)
assert mgr.get("civitai_api_key") == "secret" assert mgr.get("civitai_api_key") == "secret"
@@ -59,3 +70,21 @@ def test_delete_setting(manager):
manager.set("example", 1) manager.set("example", 1)
manager.delete("example") manager.delete("example")
assert manager.get("example") is None assert manager.get("example") is None
def test_migrates_legacy_settings_file(tmp_path, monkeypatch):
legacy_root = tmp_path / "legacy"
legacy_root.mkdir()
legacy_file = legacy_root / "settings.json"
legacy_file.write_text("{\"value\": 1}", encoding="utf-8")
target_dir = tmp_path / "config"
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(legacy_root))
monkeypatch.setattr(settings_paths, "user_config_dir", lambda *_, **__: str(target_dir))
migrated_path = settings_paths.ensure_settings_file()
assert migrated_path == str(target_dir / "settings.json")
assert (target_dir / "settings.json").exists()
assert not legacy_file.exists()