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:
Will Miao
2026-10-10 07:41:48 +08:00
parent 7937aeaa04
commit af90fe873d
17 changed files with 622 additions and 58 deletions
+98 -53
View File
@@ -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,