add delete function

This commit is contained in:
yorkane
2025-12-01 22:07:55 +08:00
parent 9bdff9a7bb
commit a0b97e92b9
2 changed files with 251 additions and 73 deletions
+123 -52
View File
@@ -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:
+128 -21
View File
@@ -390,6 +390,16 @@ function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWid
<option value="text">📝 Text</option>
<option value="folder">📁 Folders Only</option>
</select>
<span class="ky-filter-label">Sort:</span>
<select class="ky-filter-select" id="ky-sort-select">
<option value="name">Name</option>
<option value="modified">Modified</option>
<option value="created">Created</option>
</select>
<select class="ky-filter-select" id="ky-order-select">
<option value="asc">Asc</option>
<option value="desc">Desc</option>
</select>
</div>
<input type="text" class="ky-current-path" id="ky-path-input" readonly />
<span class="ky-header-meta" id="ky-header-meta"></span>
@@ -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 = "";
}
//
//