From a0b97e92b953fdca7c9d72acbd2e2f711ef6f5af Mon Sep 17 00:00:00 2001 From: yorkane Date: Mon, 1 Dec 2025 22:07:55 +0800 Subject: [PATCH] add delete function --- py/KY_Files.py | 175 +++++++++++++++++++++++++++++++------------- web/ky_file_path.js | 149 +++++++++++++++++++++++++++++++------ 2 files changed, 251 insertions(+), 73 deletions(-) diff --git a/py/KY_Files.py b/py/KY_Files.py index fdd3648..6c4820d 100644 --- a/py/KY_Files.py +++ b/py/KY_Files.py @@ -11,6 +11,7 @@ from server import PromptServer from aiohttp import web import comfy.utils import os +import shutil from urllib.parse import quote try: import folder_paths @@ -347,60 +348,118 @@ def register_routes(): parent_path = "" # Linux 根目录没有父级 # --- 读取文件列表 --- + # 只有非根目录(Linux)或非特殊情况才添加 ".." + # 但前端通常依赖 parent_path 按钮,这里为了列表完整性可以保留, + # 或者由前端根据 parent_path 渲染返回按钮 + if parent_path: + folders.append({"name": "..", "type": "dir", "path": parent_path}) + + # 定义需要过滤的文件扩展名和目录名 + filtered_extensions = {'.py', '.pyw', '.pyc', '.pyo', '.pyd', # Python相关 + '.sh', '.bash', '.zsh', '.fish', # Shell脚本 + '.bat', '.cmd', '.ps1', '.psm1', '.psd1', # Windows批处理和PowerShell + '.ini', '.cfg', '.conf', '.config', # 配置文件 + '.yaml', '.yml', '.toml'} # 其他配置文件格式 + + filtered_dir_names = {'__pycache__', '.git', '.svn', '.hg', # 版本控制和Python缓存 + 'node_modules', '.vscode', '.idea'} # 开发工具目录 + + for item in os.listdir(path): + if item.startswith('.'): continue + + full_item_path = os.path.join(path, item) + + # 检查类型和权限 + try: + if os.path.isdir(full_item_path): + # 过滤特定目录名 + if item in filtered_dir_names: + continue + try: + mtime = os.path.getmtime(full_item_path) + except Exception: + mtime = 0 + try: + ctime = os.path.getctime(full_item_path) + except Exception: + ctime = 0 + folders.append({"name": item, "type": "dir", "path": full_item_path, "modified": mtime, "created": ctime}) + else: + # 获取文件扩展名并检查是否需要过滤 + _, ext = os.path.splitext(item.lower()) + if ext in filtered_extensions: + continue + try: + mtime = os.path.getmtime(full_item_path) + except Exception: + mtime = 0 + try: + ctime = os.path.getctime(full_item_path) + except Exception: + ctime = 0 + files.append({"name": item, "type": "file", "path": full_item_path, "modified": mtime, "created": ctime}) + except OSError: + # 忽略无权访问的文件/链接 + continue + # 排序 + folders.sort(key=lambda x: x['name'].lower()) + files.sort(key=lambda x: x['name'].lower()) + + result = folders + files + return web.json_response({ + "path": path, + "parent_path": parent_path, + "files": result + }) + + @PromptServer.instance.routes.delete("/ky_utils/delete") + async def delete_path(request): try: - # 只有非根目录(Linux)或非特殊情况才添加 ".." - # 但前端通常依赖 parent_path 按钮,这里为了列表完整性可以保留, - # 或者由前端根据 parent_path 渲染返回按钮 - if parent_path: - folders.append({"name": "..", "type": "dir", "path": parent_path}) + data = await request.json() + path = data.get("path", "") + if not path: + return web.json_response({"error": "No path provided"}, status=400) + abs_path = os.path.abspath(path) + if not os.path.exists(abs_path): + return web.json_response({"error": "Path does not exist"}, status=404) + base = os.path.basename(abs_path) + if base == "..": + return web.json_response({"error": "Invalid target"}, status=400) - # 定义需要过滤的文件扩展名和目录名 - filtered_extensions = {'.py', '.pyw', '.pyc', '.pyo', '.pyd', # Python相关 - '.sh', '.bash', '.zsh', '.fish', # Shell脚本 - '.bat', '.cmd', '.ps1', '.psm1', '.psd1', # Windows批处理和PowerShell - '.ini', '.cfg', '.conf', '.config', # 配置文件 - '.yaml', '.yml', '.toml'} # 其他配置文件格式 - - filtered_dir_names = {'__pycache__', '.git', '.svn', '.hg', # 版本控制和Python缓存 - 'node_modules', '.vscode', '.idea'} # 开发工具目录 + input_root = None + output_root = None + try: + if folder_paths is not None: + input_root = folder_paths.get_input_directory() + output_root = folder_paths.get_output_directory() + except Exception: + input_root = None + output_root = None + if not input_root: + input_root = os.path.join(os.getcwd(), "input") + if not output_root: + output_root = os.path.join(os.getcwd(), "output") - for item in os.listdir(path): - if item.startswith('.'): continue - - full_item_path = os.path.join(path, item) - - # 检查类型和权限 - try: - if os.path.isdir(full_item_path): - # 过滤特定目录名 - if item in filtered_dir_names: - continue - folders.append({"name": item, "type": "dir", "path": full_item_path}) - else: - # 获取文件扩展名并检查是否需要过滤 - _, ext = os.path.splitext(item.lower()) - if ext in filtered_extensions: - continue - files.append({"name": item, "type": "file", "path": full_item_path}) - except OSError: - # 忽略无权访问的文件/链接 - continue - - # 排序 - folders.sort(key=lambda x: x['name'].lower()) - files.sort(key=lambda x: x['name'].lower()) - - result = folders + files - return web.json_response({ - "path": path, - "parent_path": parent_path, # 关键:让后端告诉前端父级在哪里 - "files": result - }) - - except PermissionError: - return web.json_response({"error": "Permission Denied", "path": path, "files": []}) + roots = [os.path.abspath(input_root), os.path.abspath(output_root)] + try: + allowed = any(os.path.commonpath([abs_path, r]) == r for r in roots if os.path.exists(r)) + except Exception: + allowed = False + if not allowed: + return web.json_response({"error": "Deletion not permitted outside input/output"}, status=403) + + try: + if os.path.isfile(abs_path): + os.remove(abs_path) + elif os.path.isdir(abs_path): + shutil.rmtree(abs_path) + else: + return web.json_response({"error": "Unsupported path type"}, status=400) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + return web.json_response({"ok": True}) except Exception as e: - return web.json_response({"error": str(e), "path": path, "files": []}) + return web.json_response({"error": str(e)}, status=500) except Exception as e: print(f"KY_GetFromPath: Error registering routes: {e}") @@ -463,9 +522,13 @@ def register_routes(): snippet = data_bytes.decode("utf-8", errors="replace") except Exception: snippet = "" + preview_url = f"/ky_utils/stream/{quote(os.path.basename(path))}?path={quote(path)}" elif ftype in ("audio","video"): can_preview = True preview_url = f"/ky_utils/stream/{quote(os.path.basename(path))}?path={quote(path)}" + else: + # 其他类型也允许下载 + preview_url = f"/ky_utils/stream/{quote(os.path.basename(path))}?path={quote(path)}" return web.json_response({ "type": ftype, "size": size, @@ -499,6 +562,8 @@ def register_routes(): image_ext = {"jpg","jpeg","png","gif","bmp","svg","webp","ico","tiff","tif"} audio_ext = {"mp3","wav","flac","aac","ogg","wma","m4a","opus"} video_ext = {"mp4","avi","mkv","mov","wmv","flv","webm","m4v","3gp","ogv"} + text_ext = {"txt","md","rtf","log","csv","tsv","json","xml"} + text_ext = {"txt","md","rtf","log","csv","tsv","json","xml"} mime = "application/octet-stream" mime_map = { "jpg": "image/jpeg", @@ -573,7 +638,10 @@ def register_routes(): "Content-Disposition": f"inline; filename=\"{filename}\"", } return web.Response(body=content, headers=headers, content_type=mime) - return web.Response(status=415) + else: + with open(path, "rb") as f: + content = f.read() + return web.Response(body=content, content_type=mime, headers={"Content-Disposition": f"inline; filename=\"{filename}\""}) except Exception: return web.Response(status=500) @@ -671,7 +739,10 @@ def register_routes(): "Content-Disposition": f"inline; filename=\"{filename}\"", } return web.Response(body=content, headers=headers, content_type=mime) - return web.Response(status=415) + else: + with open(path, "rb") as f: + content = f.read() + return web.Response(body=content, content_type=mime, headers={"Content-Disposition": f"inline; filename=\"{filename}\""}) except Exception: return web.Response(status=500) except Exception as e: diff --git a/web/ky_file_path.js b/web/ky_file_path.js index df5271f..d3ebc18 100644 --- a/web/ky_file_path.js +++ b/web/ky_file_path.js @@ -390,6 +390,16 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid + Sort: + + @@ -417,6 +427,8 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid const cancelBtn = content.querySelector("#ky-cancel-btn"); const selectBtn = content.querySelector("#ky-select-btn"); const filterSelect = content.querySelector("#ky-filter-select"); + const sortSelect = content.querySelector("#ky-sort-select"); + const orderSelect = content.querySelector("#ky-order-select"); const previewEl = content.querySelector("#ky-preview"); const previewContentEl = content.querySelector("#ky-preview-content"); const headerMetaEl = content.querySelector("#ky-header-meta"); @@ -426,6 +438,8 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid let parentPath = ""; // 由后端 API 提供 let selectedItemPath = null; let currentFilter = "all"; // 当前过滤类型 + let currentSortKey = "name"; + let currentSortOrder = "asc"; let allFiles = []; // 存储所有文件,用于过滤 let initialFilePath = filePathToPreview; // 存储初始文件路径,用于预览 let renderedFiles = []; @@ -619,7 +633,9 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid } } const info = cached.info; - const sizeStr = typeof info?.size === "number" ? `${info.size} bytes` : ""; + const mb = 1024*1024; + //如果字节数小于1024则不加单位,如果大于1024则用KB, 如果大于1024*1024则用MB + const sizeStr = typeof info?.size === "number" ? `${(info.size>mb?info.size/mb:info.size>1024?info.size/1024:info.size).toFixed(2)} ${info.size>mb?"MB":"KB"}` : ""; if (headerMetaEl) headerMetaEl.textContent = `${file.name}${sizeStr ? ` • ${sizeStr}` : ""}`; if (downloadBtn) { if (info?.preview_url) { @@ -696,6 +712,30 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid // 根据当前过滤条件筛选文件 const filteredFiles = allFiles.filter(file => shouldShowFile(file, currentFilter)); + + // 排序 + const orderFactor = currentSortOrder === "desc" ? -1 : 1; + filteredFiles.sort((a, b) => { + if (a && a.name === "..") return -1; + if (b && b.name === "..") return 1; + const aDir = a && (a.type === "dir" || a.type === "drive"); + const bDir = b && (b.type === "dir" || b.type === "drive"); + if (aDir && !bDir) return -1; + if (!aDir && bDir) return 1; + if (currentSortKey === "name") { + const an = (a.name || "").toLowerCase(); + const bn = (b.name || "").toLowerCase(); + return an.localeCompare(bn) * orderFactor; + } + const av = typeof a[currentSortKey] === "number" ? a[currentSortKey] : 0; + const bv = typeof b[currentSortKey] === "number" ? b[currentSortKey] : 0; + if (av === bv) { + const an = (a.name || "").toLowerCase(); + const bn = (b.name || "").toLowerCase(); + return an.localeCompare(bn) * orderFactor; + } + return (av - bv) * orderFactor; + }); renderedFiles = filteredFiles; filteredFiles.forEach((file, i) => { @@ -713,31 +753,30 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid el.dataset.index = String(i); el.onclick = () => { - // 如果是文件夹或驱动器,点击进入 - // 如果是 ".." 也是进入 - const isNavigable = file.type === "dir" || file.type === "drive"; - - if (isNavigable && file.name !== "..") { + // ".." 保持为进入上级 + if (file.name === "..") { fetchPath(file.path); - } else if (file.name === "..") { - // 使用后端返回的 parent_path 会更稳,但点击列表中 .. 时通常 file.path 已经是正确父路径 - fetchPath(file.path); - } else { - // 选中文件 - document.querySelectorAll(".ky-file-item").forEach(i => i.classList.remove("selected")); - el.classList.add("selected"); - selectedItemPath = file.path; - pathInput.value = file.path; - // 清除初始文件路径,因为用户已经手动选择了文件 - initialFilePath = null; + return; + } + // 单击:文件与文件夹都可选中 + document.querySelectorAll(".ky-file-item").forEach(i => i.classList.remove("selected")); + el.classList.add("selected"); + selectedItemPath = file.path; + pathInput.value = file.path; + initialFilePath = null; + if (file.type === "file") { updatePreview(file); + } else { + clearPreview(); } }; - if (file.type === "file") { - el.ondblclick = () => { + el.ondblclick = () => { + if (file.type === "file") { finalizeSelection(file.path); - }; - } + } else if (file.type === "dir" || file.type === "drive" || file.name === "..") { + fetchPath(file.path); + } + }; fileListEl.appendChild(el); }); @@ -777,6 +816,69 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid applyFilter(); }; + sortSelect.onchange = () => { + currentSortKey = sortSelect.value; + applyFilter(); + }; + + orderSelect.onchange = () => { + currentSortOrder = orderSelect.value; + applyFilter(); + }; + + async function deleteSelected() { + try { + if (!currentDialog || !document.body.contains(currentDialog)) return; + if (!selectedItemPath) return; + const target = renderedFiles.find(f => f.path === selectedItemPath); + if (!target) return; + if (target.name === "..") return; + if (target.type === "drive") return; + if (target.type === "dir") { + const ok = confirm(`Delete folder "${target.name}"? This cannot be undone.`); + if (!ok) return; + } + const resp = await api.fetchApi("/ky_utils/delete", { + method: "DELETE", + body: JSON.stringify({ path: target.path }) + }); + const data = await resp.json(); + if (data && data.ok) { + const startIdx = renderedFiles.findIndex(f => f.path === selectedItemPath); + const wasFile = target.type === "file"; + allFiles = allFiles.filter(f => f.path !== target.path); + selectedItemPath = null; + applyFilter(); + if (wasFile && renderedFiles && renderedFiles.length > 0) { + let anchor = startIdx; + if (anchor < 0) anchor = findFirstFileIndex(); + if (anchor > renderedFiles.length - 1) anchor = renderedFiles.length - 1; + const candidate = renderedFiles[anchor]; + if (candidate && candidate.type === "file") { + selectFileAndPreview(candidate); + } else { + let nextIdx = findNextFileIndex(anchor, 1); + if (renderedFiles[nextIdx] && renderedFiles[nextIdx].type === "file") { + selectFileAndPreview(renderedFiles[nextIdx]); + } else { + nextIdx = findNextFileIndex(anchor, -1); + if (renderedFiles[nextIdx] && renderedFiles[nextIdx].type === "file") { + selectFileAndPreview(renderedFiles[nextIdx]); + } else { + clearPreview(); + if (headerMetaEl) headerMetaEl.textContent = ""; + } + } + } + } + } else { + alert(`Delete failed: ${data?.error || "Unknown error"}`); + } + } catch (err) { + alert(`Delete failed: ${err?.message || err}`); + } + } + // 添加键盘事件监听器,只在对话框打开时有效 const keyHandler = (e) => { // 确保事件只在对话框打开时处理 @@ -820,6 +922,10 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid e.stopPropagation(); const idx = findLastFileIndex(); selectFileAndPreview(renderedFiles[idx]); + } else if (e.key === "Delete") { + e.preventDefault(); + e.stopPropagation(); + deleteSelected(); } }; @@ -839,6 +945,7 @@ function clearPreview() { if (previewContentEl) previewContentEl.innerHTML = ""; } + // //