PLWL/src/fsutil.js

232 lines
5.8 KiB
JavaScript

'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 }