show progress for imports and deletions
This commit is contained in:
parent
38eea05789
commit
5e342b6fe5
13 changed files with 343 additions and 115 deletions
182
src/fsutil.js
182
src/fsutil.js
|
|
@ -1,6 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const fsp = require('fs').promises
|
||||
const path = require('path')
|
||||
|
||||
// Returns a non-existing path in `base` named `name` (or `name (2)`, `name (3)`...).
|
||||
|
|
@ -12,4 +13,183 @@ function dedupeDir(base, name) {
|
|||
return path.join(base, `${name} (${n})`)
|
||||
}
|
||||
|
||||
module.exports = { dedupeDir }
|
||||
// Progress is cheap to produce and expensive to render: hashing or copying a
|
||||
// tree can emit tens of thousands of events, and forwarding every single one
|
||||
// over IPC makes the window stutter instead of showing progress. Events are
|
||||
// rate limited to ~10/s; `force` lets a caller push the value that matters
|
||||
// (the end of a unit) through immediately.
|
||||
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
|
||||
}
|
||||
|
||||
// What a tree copy will have to move. Symlinks have no size of their own
|
||||
// (they are recreated, not read), so they only count for the file total.
|
||||
// `skipDir` is a single directory name to leave out (a game folder's
|
||||
// redistributables) - walking the whole tree first is fast next to the copy.
|
||||
async function treeSize(root) {
|
||||
let files = 0
|
||||
let bytes = 0
|
||||
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 directory tree the way an imported game/wine build has to arrive:
|
||||
// symlinks recreated as they are (copyFile would silently drop them) and file
|
||||
// permissions carried over (copyFile writes 0644, killing scripts and any
|
||||
// binary the game executes itself). Reports cumulative { done, bytes } after
|
||||
// every file through `onProgress`, so long copies can drive a progress bar.
|
||||
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 a tree deepest-first so a long removal (a multi-GB prefix) can drive
|
||||
// a progress bar instead of freezing the window on one rmSync. Symlinks are
|
||||
// unlinked, never followed. Reports cumulative { files, bytes, dirs } through
|
||||
// `onProgress`; a missing tree is simply done.
|
||||
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)
|
||||
}
|
||||
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 = { dedupeDir, copyTree, treeSize, removeTree, progressGate }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue