'use strict' const fs = require('fs') const fsp = require('fs').promises const path = require('path') // accessSync instead of existsSync: same answer, one syscall fewer. function pathExists(p) { try { fs.accessSync(p) return true } catch { return false } } // Returns a non-existing path in `base` named `name` (or `name (2)`, `name (3)`...). function dedupeDir(base, name) { const candidate = path.join(base, name) if (!fs.existsSync(candidate)) return candidate let n = 2 while (fs.existsSync(path.join(base, `${name} (${n})`))) n++ return path.join(base, `${name} (${n})`) } // Events are rate limited to ~10/s; `force` pushes the final value through. const PROGRESS_MIN_MS = 100 function progressGate(emit) { let last = 0 let pending = null let timer = null const flush = () => { timer = null if (!pending) return last = Date.now() const data = pending pending = null emit(data) } const push = (data, force) => { pending = data const wait = PROGRESS_MIN_MS - (Date.now() - last) if (force || wait <= 0) { if (timer) clearTimeout(timer) flush() } else if (!timer) { timer = setTimeout(flush, wait) if (timer.unref) timer.unref() } } push.done = () => { if (timer) clearTimeout(timer) flush() } return push } // 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) let next = 0 const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { while (next < items.length) { const i = next++ out[i] = await fn(items[i], i) } }) await Promise.all(workers) return out } // What a tree copy will move. Symlinks are recreated, not read. `skipDir` // leaves out one directory name (redistributables). async function treeSize(root) { let files = 0 let bytes = 0 // a symlinked root is the whole tree; never measure through it let rootStat = null try { rootStat = await fsp.lstat(root) } catch { return { files, bytes } } if (!rootStat.isDirectory()) return { files: 1, bytes: rootStat.isSymbolicLink() ? 0 : rootStat.size } const walk = async dir => { let entries try { entries = await fsp.readdir(dir, { withFileTypes: true }) } catch { return } for (const entry of entries) { const full = path.join(dir, entry.name) if (entry.isSymbolicLink()) { files++ } else if (entry.isDirectory()) { await walk(full) } else if (entry.isFile()) { files++ try { bytes += (await fsp.stat(full)).size } catch {} } } } await walk(root) return { files, bytes } } // Copy a tree with symlinks recreated and permissions carried (copyFile // drops both); reports cumulative { done, bytes } through onProgress. async function copyTree(src, dst, onProgress) { let done = 0 let bytes = 0 const gate = onProgress ? progressGate(onProgress) : null const tick = () => { if (gate) gate({ done, bytes }, false) } const walk = async (from, to) => { let entries try { entries = await fsp.readdir(from, { withFileTypes: true }) } catch { return } await fsp.mkdir(to, { recursive: true }) for (const entry of entries) { const s = path.join(from, entry.name) const d = path.join(to, entry.name) if (entry.isSymbolicLink()) { let target = null try { target = await fsp.readlink(s) } catch {} if (!target) continue try { await fsp.symlink(target, d) } catch {} done++ tick() } else if (entry.isDirectory()) { await walk(s, d) } else if (entry.isFile()) { let size = 0 try { size = (await fsp.stat(s)).size } catch {} try { await fsp.copyFile(s, d) await fsp.chmod(d, (await fsp.stat(s)).mode & 0o777) } catch {} done++ bytes += size tick() } } } await walk(src, dst) if (gate) gate({ done, bytes }, true) return { done, bytes } } // Delete deepest-first with progress through onProgress; symlinks are // unlinked, never followed. async function removeTree(root, onProgress) { let files = 0 let dirs = 0 let bytes = 0 const gate = onProgress ? progressGate(onProgress) : null const tick = () => { if (gate) gate({ files, dirs, bytes }, false) } // a symlinked root is removed as itself, never walked through let rootStat = null try { rootStat = await fsp.lstat(root) } catch { return { files, dirs, bytes } } if (!rootStat.isDirectory()) { files = 1 bytes = rootStat.isSymbolicLink() ? 0 : rootStat.size try { await fsp.unlink(root) } catch {} if (gate) gate({ files, dirs, bytes }, true) return { files, dirs, bytes } } const walk = async dir => { let entries try { entries = await fsp.readdir(dir, { withFileTypes: true }) } catch { return false } for (const entry of entries) { const full = path.join(dir, entry.name) if (entry.isDirectory()) { await walk(full) try { await fsp.rmdir(full) } catch {} dirs++ tick() continue } let size = 0 try { const st = await fsp.lstat(full) size = st.isFile() ? st.size : 0 } catch {} try { await fsp.unlink(full) } catch {} files++ bytes += size tick() } try { await fsp.rmdir(dir) dirs++ } catch {} return true } await walk(root) if (gate) gate({ files, dirs, bytes }, true) return { files, dirs, bytes } } module.exports = { pathExists, dedupeDir, copyTree, treeSize, removeTree, progressGate, mapLimit }