'use strict' const path = require('path') const fs = require('fs') const { DIRS, readJson, writeJson } = require('./paths') const { rewireSymlinks } = require('./symlinks') const { removeTree, treeSize } = require('./fsutil') const { normalizePathName } = require('./names') // An app path stored relative to its prefix root ("drive_c/..." or // "pfx/drive_c/..."); everything else is FILES-relative or absolute. const PREFIX_REL_RE = /^(pfx[/\\])?drive_c([/\\]|$)/i const isPrefixRel = v => typeof v === 'string' && PREFIX_REL_RE.test(v) const REGISTRY = path.join(DIRS.DATA, 'prefixes.json') const MAIN = 'Main' function seed() { const defs = readJson(REGISTRY, null) if (!defs || !Array.isArray(defs.prefixes) || !defs.prefixes.length) { const seeded = { default: MAIN, prefixes: [{ name: MAIN, created: Date.now(), locked: true }] } if (!fs.existsSync(prefixPath(MAIN))) fs.mkdirSync(prefixPath(MAIN), { recursive: true }) ensureUnifiedPrefix(MAIN) writeJson(REGISTRY, seeded) } } function listPrefixes() { seed() return readJson(REGISTRY, { default: MAIN, prefixes: [] }) } function savePrefixes(defs) { writeJson(REGISTRY, defs) } function prefixPath(name) { return path.join(DIRS.PREFIXES, name) } // Native Linux apps keep their redirected home/config/cache in here, next to // the Windows side of the same prefix. const LINUX_PFX = 'linux_pfx' function prefixLinuxRoot(name) { return path.join(prefixPath(name), LINUX_PFX) } function prefixExists(name) { try { fs.accessSync(prefixPath(name)) return true } catch { return false } } // Every Windows C: drive a prefix has (top-level for plain wine, under pfx/ // for Proton); a unified prefix reports its single drive once. function prefixDriveRoots(name) { const root = prefixPath(name) const found = [] const seen = new Set() for (const base of [root, path.join(root, 'pfx')]) { try { const driveC = path.join(base, 'drive_c') if (!fs.statSync(driveC).isDirectory()) continue const real = fs.realpathSync(driveC) if (seen.has(real)) continue seen.add(real) found.push(driveC) } catch {} } return found } function lstatOrNull(p) { try { return fs.lstatSync(p) } catch { return null } } // Without a link, a prefix used with both wine kinds grows two C: drives; // `pfx` -> prefix root collapses them. Only a prefix with no pfx at all gets // the link - an existing layout is left exactly as it is. function ensureUnifiedPrefix(name) { if (!name) return false const pfx = path.join(prefixPath(name), 'pfx') if (lstatOrNull(pfx)) return false try { fs.symlinkSync('.', pfx, 'dir') return true } catch { // the prefix folder may not exist yet - the link is added on creation return false } } // True when a path IS one of a prefix's own roots (folder, pfx/, drive_c): // importing one would gut the prefix. function isPrefixRoot(p) { if (!p) return false const abs = path.resolve(String(p)) if (path.resolve(DIRS.PREFIXES) === abs) return true for (const entry of listPrefixes().prefixes) { const root = prefixPath(entry.name) if (abs === root || abs === path.join(root, 'pfx')) return true if (abs === path.join(root, 'drive_c') || abs === path.join(root, 'pfx', 'drive_c')) return true if (abs === path.join(root, LINUX_PFX) || abs === path.join(root, LINUX_PFX, 'home')) return true } return false } function createPrefix(name) { const clean = normalizePathName(name) if (!clean) return { ok: false, error: 'invalidName' } if (clean.toLowerCase() === MAIN.toLowerCase()) return { ok: false, error: 'alreadyExists' } if (fs.existsSync(prefixPath(clean))) return { ok: false, error: 'alreadyExists' } fs.mkdirSync(prefixPath(clean), { recursive: true }) ensureUnifiedPrefix(clean) const defs = listPrefixes() defs.prefixes.push({ name: clean, created: Date.now() }) if (!defs.prefixes.some(p => p.name === defs.default)) defs.default = clean savePrefixes(defs) return { ok: true, name: clean } } // Removal is measured (staging) so the UI shows a bar that moves. async function deletePrefix(name, onProgress) { if (String(name).toLowerCase() === MAIN.toLowerCase()) return { ok: false, error: 'isMain' } const defs = listPrefixes() const emit = (phase, detail) => { if (onProgress) onProgress({ phase, label: name, ...detail }) } emit('count', { files: 0, bytes: 0 }) const total = await treeSize(prefixPath(name)) await removeTree(prefixPath(name), p => emit('remove', { files: p.files, bytes: p.bytes, totalFiles: total.files, totalBytes: total.bytes })) // the registry forgets it only once the folder is gone, so an interrupted // delete stays visible and can be retried defs.prefixes = defs.prefixes.filter(p => p.name !== name) if (defs.default === name) defs.default = MAIN savePrefixes(defs) return { ok: true } } // Rewire absolute links to relative so a moved prefix keeps working. async function healPrefix(name) { if (!name) return 0 return rewireSymlinks(prefixPath(name), [DIRS.FILES]) } // Rename moves the folder and follows the name through the registry. function renamePrefix(oldName, newName) { const clean = normalizePathName(newName) if (!clean) return { ok: false, error: 'invalidName' } const defs = listPrefixes() const entry = defs.prefixes.find(p => p.name === oldName) if (!entry) return { ok: false, error: 'notFound' } if (entry.locked || String(oldName).toLowerCase() === MAIN.toLowerCase()) return { ok: false, error: 'isMain' } if (clean === oldName) return { ok: true, name: clean, games: 0 } if (defs.prefixes.some(p => p.name === clean) || fs.existsSync(prefixPath(clean))) return { ok: false, error: 'alreadyExists' } // a registered prefix without a folder yet can still be renamed if (fs.existsSync(prefixPath(oldName))) { try { fs.renameSync(prefixPath(oldName), prefixPath(clean)) } catch (err) { return { ok: false, error: String((err && err.message) || err) } } } entry.name = clean if (defs.default === oldName) defs.default = clean savePrefixes(defs) const games = require('./library').renamePrefixRefs(oldName, clean) return { ok: true, name: clean, games } } function setDefaultPrefix(name) { const defs = listPrefixes() if (name && !defs.prefixes.some(p => p.name === name)) return { ok: false, error: 'notFound' } defs.default = name || '' savePrefixes(defs) return { ok: true } } module.exports = { isPrefixRel, PREFIX_REL_RE, LINUX_PFX, listPrefixes, savePrefixes, createPrefix, deletePrefix, renamePrefix, setDefaultPrefix, prefixPath, prefixLinuxRoot, prefixExists, prefixDriveRoots, isPrefixRoot, healPrefix, ensureUnifiedPrefix, MAIN }