'use strict' const fsp = require('fs').promises const path = require('path') const { DIRS, resolvePortablePath } = require('./paths') const { newGame, newGameId, addGame } = require('./library') const { extractIconFromExe } = require('./icons') // Directories that typically hold installers/redistributables, not the game. const SKIPPED_DIRS = new Set(['redist', 'directx', '_commonredist', 'support', 'dotnet', 'vc']) 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.isDirectory()) { await copyRecursive(s, d, onProgress) } else if (e.isFile()) { await fsp.copyFile(s, d) if (onProgress) onProgress(s) } } }) } function moveFolder(src, dst, onProgress) { const fallback = () => copyRecursive(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) } async function importGame(srcDir, mode, onProgress) { const name = path.basename(srcDir) // Validate before touching the user's files: refuse an import without any // .exe (and don't half-move a folder we'd then have to roll back). const preExe = await findDefaultExe(srcDir) if (!preExe) return { ok: false, error: 'no-exe-found' } // The id doubles as the imported folder name, so generate it first and // create files/apps/ - no dedupe suffixes, ids can never collide. const id = newGameId() 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) workdir = dst } const exePath = (await findDefaultExe(workdir)) || preExe if (!exePath) { return { ok: false, error: 'no-exe-found' } } const game = newGame({ id, name, exePath, workdir, mode }) game.imported = mode === 'copy' || mode === 'move' // the exe was picked automatically -> grab its icon right away; a missing // icon is not an import failure (game can still get one from config) const icon = path.join(DIRS.DATA, 'icons', id, 'icon.ico') if (extractIconFromExe(resolvePortablePath(exePath), icon).ok) { game.iconSource = icon } return addGame(game) } // Largest .exe in the tree, skipping redistributable folders. Files are // stat'ed in parallel and folders walked breadth-first so big games don't // turn the picker into a sequential crawl. async function findDefaultExe(dir) { let best = '' let bestSize = -1 const pending = [] const consider = full => { const size = fsp.stat(full).then(st => st.size).catch(() => -1) pending.push( size.then(sz => { if (sz > bestSize) { best = full bestSize = sz } }) ) } const walk = async d => { let entries = [] try { entries = await fsp.readdir(d, { withFileTypes: true }) } catch { return } const subdirs = [] for (const e of entries) { const full = path.join(d, e.name) if (e.isDirectory()) { if (!SKIPPED_DIRS.has(e.name.toLowerCase())) subdirs.push(full) } else if (e.name.toLowerCase().endsWith('.exe')) { consider(full) } } await Promise.all(subdirs.map(walk)) } await walk(dir) await Promise.all(pending) return best } module.exports = { importGame, findDefaultExe, copyRecursive, moveFolder }