show progress for imports and deletions

This commit is contained in:
olcxja 2026-10-02 15:19:00 +02:00
commit 5e342b6fe5
13 changed files with 343 additions and 115 deletions

View file

@ -7,45 +7,16 @@ const { newGame, newGameId, addGame } = require('./library')
const { extractIconFromExe } = require('./icons')
const { listPrefixes, prefixDriveRoot } = require('./prefixes')
const { rewireSymlinks } = require('./symlinks')
const { copyTree, treeSize } = require('./fsutil')
// Directories that typically hold installers/redistributables, not the game.
const SKIPPED_DIRS = new Set(['redist', 'directx', '_commonredist', 'support', 'dotnet', 'vc'])
// A game folder has to arrive as it was: symlinks are recreated (copyFile
// silently dropped them, so a linked asset or save folder vanished) and the
// permission bits are carried over (copyFile writes 0644, killing scripts and
// anything the game executes itself).
function copyRecursive(src, dst, onProgress) {
return fsp.readdir(src, { withFileTypes: true }).then(async entries => {
await fsp.mkdir(dst, { recursive: true })
for (const e of entries) {
const s = path.join(src, e.name)
const d = path.join(dst, e.name)
if (e.isSymbolicLink()) {
let target = null
try {
target = await fsp.readlink(s)
} catch {}
if (!target) continue
try {
await fsp.symlink(target, d)
} catch {}
if (onProgress) onProgress(s)
} else if (e.isDirectory()) {
await copyRecursive(s, d, onProgress)
} else if (e.isFile()) {
await fsp.copyFile(s, d)
try {
await fsp.chmod(d, (await fsp.stat(s)).mode & 0o777)
} catch {}
if (onProgress) onProgress(s)
}
}
})
}
// The copy itself lives in fsutil (copyTree): symlinks recreated, permissions
// carried over, byte-level progress. moveFolder only falls back to it across
// filesystems; a same-device rename reports nothing (nothing is copied).
function moveFolder(src, dst, onProgress) {
const fallback = () => copyRecursive(src, dst, onProgress).then(() => fsp.rm(src, { recursive: true, force: true }))
const fallback = () => copyTree(src, dst, onProgress).then(() => fsp.rm(src, { recursive: true, force: true }))
if (path.parse(src).root !== path.parse(dst).root) return fallback()
return fsp.rename(src, dst).catch(fallback)
}
@ -62,8 +33,15 @@ async function importGame(srcDir, mode, onProgress) {
let workdir = srcDir
if (mode === 'copy' || mode === 'move') {
const dst = path.join(DIRS.APPS, id)
if (mode === 'copy') await copyRecursive(srcDir, dst, onProgress)
else await moveFolder(srcDir, dst, onProgress)
if (mode === 'copy') {
// measured before it starts, so the bar can follow the bytes landing
const emit = (phase, detail) => {
if (onProgress) onProgress({ phase, label: name, ...detail })
}
emit('count', { files: 0, bytes: 0 })
const total = await treeSize(srcDir)
await copyTree(srcDir, dst, p => emit('copy', { files: p.done, bytes: p.bytes, totalFiles: total.files, totalBytes: total.bytes }))
} else await moveFolder(srcDir, dst, onProgress)
workdir = dst
// anything the game linked to INSIDE the launcher data (its own folder, an
// icon, a shared prefix file) becomes a relative link, so the imported app
@ -156,4 +134,4 @@ async function findDefaultExe(dir) {
return best
}
module.exports = { importGame, importGameFromPrefix, findDefaultExe, copyRecursive, moveFolder }
module.exports = { importGame, importGameFromPrefix, findDefaultExe, moveFolder }