On an AutoDL server every model in the models folder is a link into the platform's shared, read-only model disk, named like 6b4679... without an extension. Scans wrote the .info beside the link, but get_metadata resolved the path and read beside the disk's file: a scanned model showed as not scanned, under the disk's file name. Delete, edit, open folder, MCP and saved-workflow lookups resolved the file too, so they refused it as outside the models folder (or, for a link inside it, would have acted on the file behind it). path_utils.model_file_within joins a resolved folder with a bare name (require_filename, which now refuses "." and ".." too); model_reference_within resolves the subfolders of a relative path inside the root, through links as before, and takes the file as named. get_metadata reads beside the path it is given (abspath). Edits, renames and deletes act on the link; nothing writes into a model file. Checked with a link stood in by patching realpath (this computer cannot make links): the info beside the link is read and the name is the link's; "..", ".", "a/b" and a relative path leaving the root are refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
"""Shows a model file in this computer's file manager: Explorer or Finder with the file
|
|
selected, the folder on Linux. The request names the model the way the browser lists it
|
|
(type, folder index, subfolder, file name), never a path, and local_guard lets only this
|
|
computer ask (it is a POST)."""
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
from aiohttp import web
|
|
|
|
from .utils import model_file_within, require_filename, resolve_folder_subdir
|
|
|
|
|
|
def _explorer_windows():
|
|
"""Handles of the open Explorer folder windows (window class CabinetWClass)."""
|
|
import ctypes
|
|
from ctypes import wintypes
|
|
|
|
found = []
|
|
callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
|
|
def collect(hwnd, _):
|
|
name = ctypes.create_unicode_buffer(64)
|
|
ctypes.windll.user32.GetClassNameW(hwnd, name, 64)
|
|
if name.value == "CabinetWClass":
|
|
found.append(hwnd)
|
|
return True
|
|
|
|
ctypes.windll.user32.EnumWindows(callback_type(collect), 0)
|
|
return found
|
|
|
|
|
|
def _window_title(hwnd):
|
|
import ctypes
|
|
|
|
title = ctypes.create_unicode_buffer(512)
|
|
ctypes.windll.user32.GetWindowTextW(hwnd, title, 512)
|
|
return title.value
|
|
|
|
|
|
def _to_front(hwnd):
|
|
"""Windows keeps a background program (ComfyUI here) from raising a window; borrowing the
|
|
foreground window's input queue for a moment lets it."""
|
|
import ctypes
|
|
|
|
user32, kernel32 = ctypes.windll.user32, ctypes.windll.kernel32
|
|
foreground = user32.GetForegroundWindow()
|
|
theirs = user32.GetWindowThreadProcessId(foreground, None)
|
|
mine = kernel32.GetCurrentThreadId()
|
|
attached = theirs and theirs != mine and user32.AttachThreadInput(mine, theirs, True)
|
|
try:
|
|
user32.ShowWindow(hwnd, 9 if user32.IsIconic(hwnd) else 5) # SW_RESTORE / SW_SHOW
|
|
user32.BringWindowToTop(hwnd)
|
|
user32.SetForegroundWindow(hwnd)
|
|
finally:
|
|
if attached:
|
|
user32.AttachThreadInput(mine, theirs, False)
|
|
|
|
|
|
def _raise_explorer(folder, before):
|
|
"""Waits up to 3 s for the window showing `folder` (a new one, or the open one Explorer
|
|
reused) and brings it to the front."""
|
|
names = (os.path.basename(folder).lower(), os.path.normcase(folder).lower())
|
|
|
|
def shows_folder(hwnd):
|
|
# The folder's name (or full path), then " - File Explorer" in the system's language.
|
|
title = _window_title(hwnd).lower()
|
|
return any(title == name or title.startswith(name + " - ") for name in names)
|
|
|
|
for _ in range(30):
|
|
time.sleep(0.1)
|
|
windows = _explorer_windows()
|
|
fresh = [hwnd for hwnd in windows if hwnd not in before]
|
|
match = fresh or [hwnd for hwnd in windows if shows_folder(hwnd)]
|
|
if match:
|
|
_to_front(match[0])
|
|
return
|
|
|
|
|
|
def _reveal(path):
|
|
if sys.platform == "win32":
|
|
before = set(_explorer_windows())
|
|
subprocess.Popen(f'explorer /select,"{path}"')
|
|
threading.Thread(target=_raise_explorer, args=(os.path.dirname(path), before), daemon=True).start()
|
|
elif sys.platform == "darwin":
|
|
subprocess.Popen(["open", "-R", path])
|
|
else:
|
|
subprocess.Popen(["xdg-open", os.path.dirname(path)])
|
|
|
|
|
|
async def api_open_model_folder(request):
|
|
try:
|
|
data = await request.json()
|
|
except ValueError:
|
|
data = None
|
|
if not isinstance(data, dict):
|
|
return web.json_response({"status": "error", "message": "Invalid request body"}, status=400)
|
|
try:
|
|
filename = require_filename(data.get("filename", ""))
|
|
_, folder = resolve_folder_subdir(data.get("type", ""), int(data.get("path_idx", 0)), data.get("subfolder", "/"))
|
|
path = model_file_within(folder, filename)
|
|
except (ValueError, KeyError, TypeError):
|
|
return web.json_response({"status": "error", "message": "Invalid request parameters"}, status=400)
|
|
if not os.path.isfile(path):
|
|
return web.json_response({"status": "error", "message": "Model file not found"}, status=404)
|
|
try:
|
|
_reveal(path)
|
|
except OSError as error:
|
|
return web.json_response({"status": "error", "message": str(error)}, status=500)
|
|
return web.json_response({"status": "success"})
|