fix: 双击文件夹条目读取失败清空该侧面板修复名不符实对比残留、枚举改手写BFS凑满即停修复超大目录枚举耗时(0.6.2)

This commit is contained in:
2026-08-18 15:29:21 +08:00
parent 946f6b2f6a
commit 74d52ec0b0
9 changed files with 128 additions and 23 deletions
+26 -12
View File
@@ -1,5 +1,5 @@
import { readdir, stat, readFile, open } from 'fs/promises'
import { join, relative, sep } from 'path'
import { join } from 'path'
/**
* 文件夹对比的纯逻辑模块(仅依赖 node:fs,vitest 以真实临时目录直接测试)。
@@ -27,7 +27,7 @@ export interface ScanResult {
entries: FolderEntry[]
/** 因超出文件数量上限被截断(条目不完整,界面提示人工确认) */
truncated: boolean
/** 枚举发现的文件总数(含被截断未比对的文件,始终为真实总数 */
/** 枚举发现的文件总数(截断时为已遍历下限:凑满上限即停止深入,不再统计剩余子树 */
total: number
}
@@ -50,24 +50,38 @@ const SAMPLE_BYTES = 8 * 1024
*/
export const SCAN_CONCURRENCY = 16
/** 递归枚举目录下全部普通文件(跳过子目录与 symlink),返回相对路径 → 大小 */
/**
* 手写 BFS 遍历目录树收集普通文件(相对路径统一 / 分隔,跳过 symlink 防环),
* 凑满 maxFiles 即停止深入:readdir({recursive:true}) 只能枚举完整棵树、无法提前终止,
* 误选超大目录(如含 node_modules)时枚举本身耗时数秒。
* 截断后 total 为已遍历部分的文件数下限(剩余子树不再统计)。
*/
async function listFiles(
root: string,
maxFiles: number
): Promise<{ files: Map<string, number>; truncated: boolean; total: number }> {
const dirents = await readdir(root, { recursive: true, withFileTypes: true })
const rels: string[] = []
let total = 0
let truncated = false
for (const d of dirents) {
if (!d.isFile()) continue
total++
if (rels.length >= maxFiles) {
truncated = true
continue
const queue: Array<{ dir: string; prefix: string }> = [{ dir: root, prefix: '' }]
while (queue.length > 0) {
const { dir, prefix } = queue.shift()!
const dirents = await readdir(dir, { withFileTypes: true })
for (const d of dirents) {
// Dirent 为 lstat 语义:symlink 既非 file 也非 directory,天然跳过(防环)
if (d.isDirectory()) {
queue.push({ dir: join(dir, d.name), prefix: prefix === '' ? d.name : `${prefix}/${d.name}` })
} else if (d.isFile()) {
total++
if (rels.length >= maxFiles) {
truncated = true
} else {
rels.push(prefix === '' ? d.name : `${prefix}/${d.name}`)
}
}
}
// parentPath 为 Node 20.12+ 的 Dirent 属性(旧名 path
rels.push(relative(root, join(d.parentPath, d.name)).split(sep).join('/'))
// 凑满即停:不再处理队列中剩余的子目录(当前目录统计完整,保证确定性
if (truncated) break
}
// 并行 stat(libuv 线程池排队,文件数量受 maxFiles 约束)
const stats = await Promise.all(rels.map((rel) => stat(join(root, rel))))