187 lines
6.6 KiB
JavaScript
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 }
|