cleanup & sync summary

This commit is contained in:
olcxja 2026-10-19 09:26:02 +02:00
commit be4f67fb60
17 changed files with 637 additions and 207 deletions

View file

@ -56,6 +56,10 @@ function progressGate(emit) {
return push
}
// Tree walks are dominated by per-file syscalls; a handful in flight keeps a
// slow disk busy without flooding the event loop with thousands of them.
const TREE_CONCURRENCY = 16
// Run `fn` over `items` with at most `limit` in flight, results in order.
async function mapLimit(items, limit, fn) {
const out = new Array(items.length)
@ -90,7 +94,7 @@ async function treeSize(root) {
} catch {
return
}
for (const entry of entries) {
await mapLimit(entries, TREE_CONCURRENCY, async entry => {
const full = path.join(dir, entry.name)
if (entry.isSymbolicLink()) {
files++
@ -102,7 +106,7 @@ async function treeSize(root) {
bytes += (await fsp.stat(full)).size
} catch {}
}
}
})
}
await walk(root)
return { files, bytes }
@ -110,9 +114,12 @@ async function treeSize(root) {
// Copy a tree with symlinks recreated and permissions carried (copyFile
// drops both); reports cumulative { done, bytes } through onProgress.
// `failed` counts entries that could not be recreated: a caller that needs a
// complete copy (an import) must not treat a partial tree as success.
async function copyTree(src, dst, onProgress) {
let done = 0
let bytes = 0
let failed = 0
const gate = onProgress ? progressGate(onProgress) : null
const tick = () => {
if (gate) gate({ done, bytes }, false)
@ -122,10 +129,16 @@ async function copyTree(src, dst, onProgress) {
try {
entries = await fsp.readdir(from, { withFileTypes: true })
} catch {
failed++
return
}
await fsp.mkdir(to, { recursive: true })
for (const entry of entries) {
try {
await fsp.mkdir(to, { recursive: true })
} catch {
failed++
return
}
await mapLimit(entries, TREE_CONCURRENCY, async entry => {
const s = path.join(from, entry.name)
const d = path.join(to, entry.name)
if (entry.isSymbolicLink()) {
@ -133,32 +146,46 @@ async function copyTree(src, dst, onProgress) {
try {
target = await fsp.readlink(s)
} catch {}
if (!target) continue
if (!target) {
failed++
return
}
try {
await fsp.symlink(target, d)
} catch {}
} catch {
failed++
}
done++
tick()
} else if (entry.isDirectory()) {
await walk(s, d)
} else if (entry.isFile()) {
let size = 0
let st = null
try {
size = (await fsp.stat(s)).size
st = await fsp.stat(s)
} catch {}
if (!st) {
failed++
return
}
try {
await fsp.copyFile(s, d)
await fsp.chmod(d, (await fsp.stat(s)).mode & 0o777)
} catch {
failed++
return
}
try {
await fsp.chmod(d, st.mode & 0o777)
} catch {}
done++
bytes += size
bytes += st.size
tick()
}
}
})
}
await walk(src, dst)
if (gate) gate({ done, bytes }, true)
return { done, bytes }
return { done, bytes, failed }
}
// Imported trees come from media that lose permissions (archives, FAT, a
@ -176,13 +203,13 @@ async function chmodTree(root) {
} catch {
return
}
for (const entry of entries) {
await mapLimit(entries, TREE_CONCURRENCY, async entry => {
const full = path.join(dir, entry.name)
if (entry.isSymbolicLink()) continue
if (entry.isSymbolicLink()) return
if (entry.isDirectory()) {
await walk(full)
} else if (!entry.isFile()) {
continue
return
}
try {
const before = (await fsp.stat(full)).mode & 0o7777
@ -193,7 +220,7 @@ async function chmodTree(root) {
else files++
}
} catch {}
}
})
}
await walk(root)
return { files, dirs }
@ -275,7 +302,7 @@ async function removeTree(root, onProgress) {
} catch {
return false
}
for (const entry of entries) {
await mapLimit(entries, TREE_CONCURRENCY, async entry => {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
await walk(full)
@ -284,7 +311,7 @@ async function removeTree(root, onProgress) {
} catch {}
dirs++
tick()
continue
return
}
let size = 0
try {
@ -297,7 +324,7 @@ async function removeTree(root, onProgress) {
files++
bytes += size
tick()
}
})
try {
await fsp.rmdir(dir)
dirs++