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 = "";
}
+
//
//