add delete function
This commit is contained in:
+123
-52
@@ -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
@@ -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 = "";
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user