PLWL/src/prefixes.js
2026-10-19 09:26:02 +02:00

187 lines
6.6 KiB
JavaScript

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