mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-10 03:22:12 -03:00
feat(modal): open model metadata sidecar location from the model modal
Centralized sidecar storage moves .metadata.json into a mirror tree, so the Location link in the model modal could no longer reveal a model's sidecar. The Location row now shows an inline metadata icon button (centralized mode only) that calls a new POST /api/lm/models/open-sidecar-location endpoint: the backend resolves the sidecar through py/utils/sidecar_paths, reveals the .metadata.json selected in the file manager (falling back to the sidecar directory when it has not been written yet), and returns the path for clipboard mode on headless/Docker sessions. The platform reveal logic is factored into a shared FileSystemHandler._reveal_file helper. All 9 locales translated.
This commit is contained in:
@@ -77,6 +77,7 @@ from ...utils.sidecar_paths import (
|
||||
get_configured_sidecar_root,
|
||||
get_metadata_path,
|
||||
get_preview_dir,
|
||||
get_sidecar_dir,
|
||||
get_storage_mode,
|
||||
get_unmatched_sidecar_components,
|
||||
)
|
||||
@@ -3497,6 +3498,63 @@ class FileSystemHandler:
|
||||
{"success": True, "message": f"Opened folder: {path}", "path": path}
|
||||
)
|
||||
|
||||
async def _reveal_file(self, file_path: str) -> web.Response:
|
||||
"""Open the platform file manager with ``file_path`` selected."""
|
||||
if os.name == "nt":
|
||||
subprocess.Popen(["explorer", "/select,", file_path])
|
||||
elif os.name == "posix":
|
||||
if _is_docker():
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Running in Docker: Path available for copying",
|
||||
"path": file_path,
|
||||
"mode": "clipboard",
|
||||
}
|
||||
)
|
||||
elif _is_wsl():
|
||||
windows_path = _wsl_to_windows_path(file_path)
|
||||
if windows_path:
|
||||
subprocess.Popen(["explorer.exe", "/select,", windows_path])
|
||||
else:
|
||||
logger.error(
|
||||
"Failed to convert WSL path to Windows path: %s", file_path
|
||||
)
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Failed to open file location: path conversion error",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.Popen(["open", "-R", file_path])
|
||||
else:
|
||||
if not _has_gui_display():
|
||||
# Headless/SSH session: no file manager to open, so
|
||||
# hand the path to the browser for copying instead
|
||||
# of reporting a success that never happened.
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Headless session: path available for copying",
|
||||
"path": file_path,
|
||||
"mode": "clipboard",
|
||||
}
|
||||
)
|
||||
selected = _select_in_linux_file_manager(file_path)
|
||||
if not selected:
|
||||
folder = os.path.dirname(file_path)
|
||||
subprocess.Popen(["xdg-open", folder])
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Opened folder and selected file: {file_path}",
|
||||
"path": file_path,
|
||||
}
|
||||
)
|
||||
|
||||
async def open_file_location(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
data = await request.json()
|
||||
@@ -3512,63 +3570,49 @@ class FileSystemHandler:
|
||||
{"success": False, "error": "File does not exist"}, status=404
|
||||
)
|
||||
|
||||
if os.name == "nt":
|
||||
subprocess.Popen(["explorer", "/select,", file_path])
|
||||
elif os.name == "posix":
|
||||
if _is_docker():
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Running in Docker: Path available for copying",
|
||||
"path": file_path,
|
||||
"mode": "clipboard",
|
||||
}
|
||||
)
|
||||
elif _is_wsl():
|
||||
windows_path = _wsl_to_windows_path(file_path)
|
||||
if windows_path:
|
||||
subprocess.Popen(["explorer.exe", "/select,", windows_path])
|
||||
else:
|
||||
logger.error(
|
||||
"Failed to convert WSL path to Windows path: %s", file_path
|
||||
)
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Failed to open file location: path conversion error",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.Popen(["open", "-R", file_path])
|
||||
else:
|
||||
if not _has_gui_display():
|
||||
# Headless/SSH session: no file manager to open, so
|
||||
# hand the path to the browser for copying instead
|
||||
# of reporting a success that never happened.
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Headless session: path available for copying",
|
||||
"path": file_path,
|
||||
"mode": "clipboard",
|
||||
}
|
||||
)
|
||||
selected = _select_in_linux_file_manager(file_path)
|
||||
if not selected:
|
||||
folder = os.path.dirname(file_path)
|
||||
subprocess.Popen(["xdg-open", folder])
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Opened folder and selected file: {file_path}",
|
||||
}
|
||||
)
|
||||
return await self._reveal_file(file_path)
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
logger.error("Failed to open file location: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def open_model_sidecar_location(self, request: web.Request) -> web.Response:
|
||||
"""Reveal the model's ``.metadata.json`` sidecar in the file manager.
|
||||
|
||||
Sidecar/preview path derivation must go through
|
||||
:mod:`py.utils.sidecar_paths` so centralized storage mode resolves to
|
||||
the mirror tree. When the sidecar has not been written yet, the
|
||||
containing sidecar directory is opened instead.
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
file_path = data.get("file_path")
|
||||
if not file_path:
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Missing file_path parameter"},
|
||||
status=400,
|
||||
)
|
||||
file_path = os.path.abspath(file_path)
|
||||
if not os.path.isfile(file_path):
|
||||
return web.json_response(
|
||||
{"success": False, "error": "File does not exist"}, status=404
|
||||
)
|
||||
|
||||
metadata_path = get_metadata_path(file_path)
|
||||
if os.path.isfile(metadata_path):
|
||||
return await self._reveal_file(metadata_path)
|
||||
|
||||
sidecar_dir = get_sidecar_dir(file_path)
|
||||
if os.path.isdir(sidecar_dir):
|
||||
return await self._open_path(sidecar_dir)
|
||||
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Sidecar does not exist yet"},
|
||||
status=404,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
logger.error("Failed to open sidecar location: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def open_settings_location(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
settings_file = getattr(self._settings, "settings_file", None)
|
||||
@@ -4454,6 +4498,7 @@ class MiscHandlerSet:
|
||||
"open_backup_location": self.filesystem.open_backup_location,
|
||||
"open_wildcards_location": self.filesystem.open_wildcards_location,
|
||||
"open_sidecar_location": self.filesystem.open_sidecar_location,
|
||||
"open_model_sidecar_location": self.filesystem.open_model_sidecar_location,
|
||||
"browse_directory": self.filesystem.browse_directory,
|
||||
"validate_path": self.filesystem.validate_path,
|
||||
"search_custom_words": self.custom_words.search_custom_words,
|
||||
|
||||
@@ -125,6 +125,9 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/sidecars/open-location", "open_sidecar_location"
|
||||
),
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/models/open-sidecar-location", "open_model_sidecar_location"
|
||||
),
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/download-model-source", "download_model_source"
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user