fix: 文件夹对比收尾打磨 - 扫描内容比对受控并发化(16并发分批)、「返回文件对比」清空两侧面板修复旧对比残留、同目录防呆提示、total注释修正与README版本号补同步(0.6.1)
This commit is contained in:
+45
-21
@@ -27,7 +27,7 @@ export interface ScanResult {
|
||||
entries: FolderEntry[]
|
||||
/** 因超出文件数量上限被截断(条目不完整,界面提示人工确认) */
|
||||
truncated: boolean
|
||||
/** 枚举发现的文件总数(截断时为上限值) */
|
||||
/** 枚举发现的文件总数(含被截断未比对的文件,始终为真实总数) */
|
||||
total: number
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ export const FOLDER_MAX_CONTENT_BYTES = 10 * 1024 * 1024
|
||||
/** 近似判定的采样头部字节数 */
|
||||
const SAMPLE_BYTES = 8 * 1024
|
||||
|
||||
/**
|
||||
* 内容比对的受控并发数:单侧缺失/大小不同直接判定(无 IO),
|
||||
* 大小一致的条目按此并发分批读取比对(批内 Promise.all,批间串行)。
|
||||
* 大目录下避免逐文件串行等待 IO;批内左右两文件也并行,实际排队 IO 约 2 倍并发。
|
||||
*/
|
||||
export const SCAN_CONCURRENCY = 16
|
||||
|
||||
/** 递归枚举目录下全部普通文件(跳过子目录与 symlink),返回相对路径 → 大小 */
|
||||
async function listFiles(
|
||||
root: string,
|
||||
@@ -85,7 +92,8 @@ async function readHead(path: string, bytes: number): Promise<Buffer> {
|
||||
* 对比两个文件夹:按相对路径对齐条目并判定内容异同。
|
||||
* 判定规则:单侧缺失 → only;大小不同 → different;
|
||||
* 大小相同且 ≤ maxContentBytes → 全量字节比对;超过上限 → 头部 8KB 采样近似判定。
|
||||
* 目录不存在/不可读时抛出异常,由 IPC 调用方转为错误结果。
|
||||
* 大小一致的条目按 SCAN_CONCURRENCY 受控并发读取比对(输出与顺序和串行实现完全一致,
|
||||
* 任一文件读取失败仍整体抛出,由 IPC 调用方转为错误结果)。
|
||||
*/
|
||||
export async function scanFolders(
|
||||
leftDir: string,
|
||||
@@ -98,34 +106,50 @@ export async function scanFolders(
|
||||
const right = await listFiles(rightDir, maxFiles)
|
||||
|
||||
const rels = [...new Set([...left.files.keys(), ...right.files.keys()])].sort()
|
||||
const entries: FolderEntry[] = []
|
||||
for (const rel of rels) {
|
||||
// 预分配结果槽位:无 IO 的条目直接定案,需读内容的条目收集为任务并发执行
|
||||
const entries: FolderEntry[] = new Array(rels.length)
|
||||
const tasks: { idx: number; run: () => Promise<void> }[] = []
|
||||
rels.forEach((rel, idx) => {
|
||||
const lSize = left.files.get(rel)
|
||||
const rSize = right.files.get(rel)
|
||||
if (lSize === undefined) {
|
||||
entries.push({ rel, status: 'right-only', leftSize: null, rightSize: rSize ?? null })
|
||||
entries[idx] = { rel, status: 'right-only', leftSize: null, rightSize: rSize ?? null }
|
||||
} else if (rSize === undefined) {
|
||||
entries.push({ rel, status: 'left-only', leftSize: lSize, rightSize: null })
|
||||
entries[idx] = { rel, status: 'left-only', leftSize: lSize, rightSize: null }
|
||||
} else if (lSize !== rSize) {
|
||||
entries.push({ rel, status: 'different', leftSize: lSize, rightSize: rSize })
|
||||
entries[idx] = { rel, status: 'different', leftSize: lSize, rightSize: rSize }
|
||||
} else if (lSize <= maxContentBytes) {
|
||||
const lBuf = await readFile(join(leftDir, rel))
|
||||
const rBuf = await readFile(join(rightDir, rel))
|
||||
entries.push(
|
||||
lBuf.equals(rBuf)
|
||||
? { rel, status: 'same', leftSize: lSize, rightSize: rSize }
|
||||
: { rel, status: 'different', leftSize: lSize, rightSize: rSize }
|
||||
)
|
||||
tasks.push({
|
||||
idx,
|
||||
run: async () => {
|
||||
const [lBuf, rBuf] = await Promise.all([
|
||||
readFile(join(leftDir, rel)),
|
||||
readFile(join(rightDir, rel))
|
||||
])
|
||||
entries[idx] = lBuf.equals(rBuf)
|
||||
? { rel, status: 'same', leftSize: lSize, rightSize: rSize }
|
||||
: { rel, status: 'different', leftSize: lSize, rightSize: rSize }
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 超过全量比对上限:头部采样近似判定,结果带 approximate 标注
|
||||
const lHead = await readHead(join(leftDir, rel), SAMPLE_BYTES)
|
||||
const rHead = await readHead(join(rightDir, rel), SAMPLE_BYTES)
|
||||
entries.push(
|
||||
lHead.equals(rHead)
|
||||
? { rel, status: 'same', leftSize: lSize, rightSize: rSize, approximate: true }
|
||||
: { rel, status: 'different', leftSize: lSize, rightSize: rSize }
|
||||
)
|
||||
tasks.push({
|
||||
idx,
|
||||
run: async () => {
|
||||
const [lHead, rHead] = await Promise.all([
|
||||
readHead(join(leftDir, rel), SAMPLE_BYTES),
|
||||
readHead(join(rightDir, rel), SAMPLE_BYTES)
|
||||
])
|
||||
entries[idx] = lHead.equals(rHead)
|
||||
? { rel, status: 'same', leftSize: lSize, rightSize: rSize, approximate: true }
|
||||
: { rel, status: 'different', leftSize: lSize, rightSize: rSize }
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 受控并发分批执行(批内任一失败整体抛出,与串行实现语义一致)
|
||||
for (let i = 0; i < tasks.length; i += SCAN_CONCURRENCY) {
|
||||
await Promise.all(tasks.slice(i, i + SCAN_CONCURRENCY).map((t) => t.run()))
|
||||
}
|
||||
return { entries, truncated: left.truncated || right.truncated, total: Math.max(left.total, right.total) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user