Files
Azornes-Comfyui-Model-Resolver/core/file_manager.py
T

170 lines
5.8 KiB
Python

"""Cross-platform helpers for revealing local files and opening directories."""
from __future__ import annotations
import os
import platform
import shutil
import subprocess
from typing import Any, Dict
LAUNCH_HANDOFF_TIMEOUT_SECONDS = 2.0
class FileManagerError(RuntimeError):
"""Base error raised when a local file manager cannot be opened."""
class FileManagerUnavailableError(FileManagerError):
"""Raised when the platform has no supported file-manager launcher."""
class UnsupportedFileManagerPlatformError(FileManagerError):
"""Raised when the current operating system is not supported."""
def _normalized_absolute_path(path_value: str) -> str:
return os.path.realpath(os.path.abspath(os.path.normpath(path_value)))
def normalize_file_manager_path(path_value: Any) -> str:
"""Normalize a local path and recover UI-style separators when necessary.
Legacy frontend state may contain backslashes as subfolder separators. On
POSIX, a backslash is also a valid filename character, so the literal path
is always checked first. It is interpreted as a separator only when the
literal path does not exist and the converted path does.
"""
if not isinstance(path_value, (str, os.PathLike)):
raise ValueError("path must be a string")
raw_path = os.fspath(path_value)
if isinstance(raw_path, bytes):
raise ValueError("path must be a string")
if not raw_path.strip():
raise ValueError("path is required")
if "\x00" in raw_path:
raise ValueError("path contains a null byte")
normalized_path = _normalized_absolute_path(raw_path)
if os.path.exists(normalized_path):
return normalized_path
if os.name != "nt" and "\\" in raw_path:
separator_path = _normalized_absolute_path(raw_path.replace("\\", os.sep))
if os.path.exists(separator_path):
return separator_path
return normalized_path
def _find_windows_explorer() -> str:
explorer = shutil.which("explorer.exe") or shutil.which("explorer")
if explorer:
return explorer
system_root = str(os.environ.get("SYSTEMROOT") or "").strip()
if system_root:
candidate = os.path.join(system_root, "explorer.exe")
if os.path.isfile(candidate):
return candidate
raise FileManagerUnavailableError("Windows Explorer could not be found.")
def _find_launcher(*names: str) -> tuple[str, str]:
for name in names:
executable = shutil.which(name)
if executable:
return name, executable
raise FileManagerUnavailableError(
f"No supported file manager launcher was found ({', '.join(names)})."
)
def _launch(command: list[str], check_exit_code: bool = True) -> None:
kwargs: Dict[str, Any] = {
"shell": False,
"stdin": subprocess.DEVNULL,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL,
}
if os.name == "nt":
kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
try:
process = subprocess.Popen(command, **kwargs)
except (OSError, ValueError) as exc:
raise FileManagerError(f"Could not start the system file manager: {exc}") from exc
# Explorer commonly delegates the request to an existing shell process and
# exits with code 1 even though the folder opened successfully. A successful
# process creation is therefore the only reliable Windows signal.
if not check_exit_code:
return
# Most launcher commands exit immediately after handing the request to the
# desktop. Catch a fast failure without waiting for a long-running manager.
try:
return_code = process.wait(timeout=LAUNCH_HANDOFF_TIMEOUT_SECONDS)
except subprocess.TimeoutExpired:
return
except OSError as exc:
raise FileManagerError(
f"Could not confirm that the system file manager started: {exc}"
) from exc
if return_code != 0:
raise FileManagerError(
f"The system file manager launcher exited with code {return_code}."
)
def open_in_file_manager(path_value: Any, system: str | None = None) -> Dict[str, Any]:
"""Reveal a file or open a directory using the host operating system."""
target_path = normalize_file_manager_path(path_value)
if not os.path.exists(target_path):
raise FileNotFoundError(f"Path does not exist: {target_path}")
is_file = os.path.isfile(target_path)
is_directory = os.path.isdir(target_path)
if not is_file and not is_directory:
raise FileManagerError("Only regular files and directories can be opened.")
platform_name = str(system or platform.system()).strip().lower()
opened_path = os.path.dirname(target_path) if is_file else target_path
selected = False
if platform_name == "windows":
explorer = _find_windows_explorer()
command = (
[explorer, "/select,", target_path]
if is_file
else [explorer, target_path]
)
selected = is_file
launcher = "explorer"
elif platform_name == "darwin":
launcher, executable = _find_launcher("open")
command = [executable, "-R", target_path] if is_file else [executable, target_path]
selected = is_file
elif platform_name == "linux":
launcher, executable = _find_launcher("xdg-open", "gio")
command = (
[executable, opened_path]
if launcher == "xdg-open"
else [executable, "open", opened_path]
)
else:
display_name = system or platform.system() or "unknown"
raise UnsupportedFileManagerPlatformError(
f"Opening folders is not supported on {display_name}."
)
_launch(command, check_exit_code=platform_name != "windows")
return {
"platform": platform_name,
"launcher": launcher,
"opened_path": opened_path,
"selected": selected,
}