mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-10 03:22:12 -03:00
fix(filesystem): select files via FileManager1 DBus on Linux
open-file-location now asks the freedesktop org.freedesktop.FileManager1 ShowItems interface to reveal the file selected (GNOME Files, Dolphin, Nemo, Caja, recent Thunar), falling back to plain xdg-open when the interface is unavailable. Also align the Linux branch with the other location handlers: headless sessions return clipboard mode instead of silently failing.
This commit is contained in:
@@ -23,6 +23,7 @@ import tempfile
|
|||||||
import zipfile
|
import zipfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from urllib.parse import quote
|
||||||
from typing import Any, Awaitable, Callable, Dict, Mapping, Protocol, Sequence
|
from typing import Any, Awaitable, Callable, Dict, Mapping, Protocol, Sequence
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@@ -439,6 +440,35 @@ def _has_gui_display() -> bool:
|
|||||||
return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
||||||
|
|
||||||
|
|
||||||
|
def _select_in_linux_file_manager(file_path: str) -> bool:
|
||||||
|
"""Reveal ``file_path`` selected in the desktop file manager.
|
||||||
|
|
||||||
|
Uses the freedesktop ``org.freedesktop.FileManager1.ShowItems`` DBus
|
||||||
|
interface, which is supported by GNOME Files (Nautilus), KDE Dolphin,
|
||||||
|
Nemo, Caja, and recent Thunar. Returns ``False`` when the interface is
|
||||||
|
unavailable so the caller can fall back to plain ``xdg-open``.
|
||||||
|
"""
|
||||||
|
uri = "file://" + quote(file_path)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"dbus-send",
|
||||||
|
"--session",
|
||||||
|
"--print-reply",
|
||||||
|
"--dest=org.freedesktop.FileManager1",
|
||||||
|
"/org/freedesktop/FileManager1",
|
||||||
|
"org.freedesktop.FileManager1.ShowItems",
|
||||||
|
f"array:string:{uri}",
|
||||||
|
"string:",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
||||||
|
return False
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
class PromptServerProtocol(Protocol):
|
class PromptServerProtocol(Protocol):
|
||||||
"""Subset of PromptServer used by the handlers."""
|
"""Subset of PromptServer used by the handlers."""
|
||||||
|
|
||||||
@@ -3512,8 +3542,22 @@ class FileSystemHandler:
|
|||||||
elif sys.platform == "darwin":
|
elif sys.platform == "darwin":
|
||||||
subprocess.Popen(["open", "-R", file_path])
|
subprocess.Popen(["open", "-R", file_path])
|
||||||
else:
|
else:
|
||||||
folder = os.path.dirname(file_path)
|
if not _has_gui_display():
|
||||||
subprocess.Popen(["xdg-open", folder])
|
# 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(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -810,6 +810,107 @@ async def test_open_wildcards_location_creates_and_opens_directory(tmp_path, mon
|
|||||||
assert calls == [["xdg-open", str(wildcards_dir)]]
|
assert calls == [["xdg-open", str(wildcards_dir)]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_open_file_location_linux_selects_via_dbus(tmp_path, monkeypatch):
|
||||||
|
"""On a Linux desktop the handler should ask the freedesktop FileManager1
|
||||||
|
DBus service to open the folder with the file selected."""
|
||||||
|
model_file = tmp_path / "model.safetensors"
|
||||||
|
model_file.write_text("x", encoding="utf-8")
|
||||||
|
|
||||||
|
handler = FileSystemHandler(settings_service=SimpleNamespace())
|
||||||
|
|
||||||
|
monkeypatch.setenv("DISPLAY", ":0")
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||||
|
|
||||||
|
run_calls = []
|
||||||
|
popen_calls = []
|
||||||
|
|
||||||
|
def fake_run(args, **kwargs):
|
||||||
|
run_calls.append(args)
|
||||||
|
return SimpleNamespace(returncode=0)
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||||
|
monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: popen_calls.append(args))
|
||||||
|
|
||||||
|
request = FakeRequest(json_data={"file_path": str(model_file)})
|
||||||
|
response = await handler.open_file_location(request) # pyright: ignore[reportArgumentType]
|
||||||
|
payload = _json_payload(response)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert len(run_calls) == 1
|
||||||
|
dbus_cmd = run_calls[0]
|
||||||
|
assert dbus_cmd[0] == "dbus-send"
|
||||||
|
assert any(
|
||||||
|
arg.startswith("array:string:file://") and "model.safetensors" in arg
|
||||||
|
for arg in dbus_cmd
|
||||||
|
)
|
||||||
|
assert popen_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_open_file_location_linux_falls_back_to_xdg_open(tmp_path, monkeypatch):
|
||||||
|
"""When the FileManager1 DBus interface is unavailable, fall back to
|
||||||
|
opening the containing folder with xdg-open."""
|
||||||
|
model_file = tmp_path / "model.safetensors"
|
||||||
|
model_file.write_text("x", encoding="utf-8")
|
||||||
|
|
||||||
|
handler = FileSystemHandler(settings_service=SimpleNamespace())
|
||||||
|
|
||||||
|
monkeypatch.setenv("DISPLAY", ":0")
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||||
|
|
||||||
|
popen_calls = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=1)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
subprocess, "Popen", lambda *args, **kwargs: popen_calls.append(args[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
request = FakeRequest(json_data={"file_path": str(model_file)})
|
||||||
|
response = await handler.open_file_location(request) # pyright: ignore[reportArgumentType]
|
||||||
|
payload = _json_payload(response)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert popen_calls == [["xdg-open", str(tmp_path)]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_open_file_location_headless_returns_clipboard_mode(tmp_path, monkeypatch):
|
||||||
|
"""Without a GUI session there is no file manager to open; the handler
|
||||||
|
must hand the path to the browser instead of failing silently."""
|
||||||
|
model_file = tmp_path / "model.safetensors"
|
||||||
|
model_file.write_text("x", encoding="utf-8")
|
||||||
|
|
||||||
|
handler = FileSystemHandler(settings_service=SimpleNamespace())
|
||||||
|
|
||||||
|
monkeypatch.delenv("DISPLAY", raising=False)
|
||||||
|
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||||
|
|
||||||
|
popen_calls = []
|
||||||
|
run_calls = []
|
||||||
|
monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: popen_calls.append(args))
|
||||||
|
monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: run_calls.append(args))
|
||||||
|
|
||||||
|
request = FakeRequest(json_data={"file_path": str(model_file)})
|
||||||
|
response = await handler.open_file_location(request) # pyright: ignore[reportArgumentType]
|
||||||
|
payload = _json_payload(response)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["mode"] == "clipboard"
|
||||||
|
assert payload["path"] == str(model_file)
|
||||||
|
assert popen_calls == []
|
||||||
|
assert run_calls == []
|
||||||
|
|
||||||
|
|
||||||
class RecordingRouter:
|
class RecordingRouter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|||||||
Reference in New Issue
Block a user