PLWL/src/paths.js

158 lines
4.7 KiB
JavaScript

'use strict'
const path = require('path')
const fs = require('fs')
// Packaged: resources/app.asar two levels below the portable root.
function computeRoots() {
const m = __dirname.match(/^(.*)[/\\]app\.asar([/\\]|$)/)
if (m) {
const RESOURCES = path.resolve(m[1])
const APP_ROOT = path.resolve(RESOURCES, '..', '..')
return {
RESOURCES,
APP_ROOT,
// FILES = BUILD/files, so BUILD is the portable root itself.
BUILD: process.env.PLWL_BUILD_DIR || APP_ROOT,
// bundled release files live inside the asar
BUNDLED: fs.existsSync(path.join(RESOURCES, 'app.asar', 'files', 'translations'))
? path.join(RESOURCES, 'app.asar', 'files')
: path.join(APP_ROOT, 'files')
}
}
const APP_ROOT = path.resolve(__dirname, '..')
return {
RESOURCES: APP_ROOT,
APP_ROOT,
BUILD: process.env.PLWL_BUILD_DIR || path.join(APP_ROOT, 'build'),
BUNDLED: path.join(APP_ROOT, 'files')
}
}
const { RESOURCES, APP_ROOT, BUILD, BUNDLED } = computeRoots()
const FILES = path.join(BUILD, 'files')
const DATA = path.join(FILES, 'data')
const DIRS = {
BUILD,
APP_ROOT,
FILES,
DATA,
WINES: path.join(FILES, 'wines'),
PREFIXES: path.join(FILES, 'wineprefixes'),
APPS: path.join(FILES, 'apps'),
TRANSLATIONS: path.join(FILES, 'translations'),
BUNDLED_TRANSLATIONS: path.join(BUNDLED, 'translations'),
FAKE_STEAM: path.join(DATA, 'fake-steam')
}
// Bundled translations are refreshed on start whenever their content changed,
// so updates reach existing installs; user-added languages are untouched.
function filesDiffer(a, b) {
try {
// compare as text: Buffer.equals() throws on a string
return fs.readFileSync(a, 'utf8') !== fs.readFileSync(b, 'utf8')
} catch {
return true // target missing/unreadable -> needs a (re)copy
}
}
function syncBundledTranslations(bundledDir, targetDir) {
let names = []
try {
names = fs.readdirSync(bundledDir)
} catch {
return 0
}
let updated = 0
for (const name of names) {
const src = path.join(bundledDir, name)
const dst = path.join(targetDir, name)
try {
if (!fs.statSync(src).isFile()) continue
} catch {
continue
}
if (!filesDiffer(src, dst)) continue
try {
fs.copyFileSync(src, dst)
updated++
} catch {}
}
return updated
}
function ensureDirs() {
for (const dir of [FILES, DATA, DIRS.WINES, DIRS.PREFIXES, DIRS.APPS, DIRS.TRANSLATIONS, DIRS.FAKE_STEAM]) {
fs.mkdirSync(dir, { recursive: true })
}
if (DIRS.TRANSLATIONS !== DIRS.BUNDLED_TRANSLATIONS) {
syncBundledTranslations(DIRS.BUNDLED_TRANSLATIONS, DIRS.TRANSLATIONS)
}
}
// Anything under FILES/ is stored FILES-relative; absolute paths outside are
// kept as-is. Resolve on read, normalize on write.
function toPortablePath(p) {
if (typeof p !== 'string' || !p || !path.isAbsolute(p)) return p
const rel = path.relative(FILES, p)
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return p
return rel
}
function resolvePortablePath(p) {
if (typeof p !== 'string' || !p || path.isAbsolute(p)) return p
return path.join(FILES, p)
}
// The launcher's own icon (renderer/plwl.png) for tray, app icon, shortcut
// fallback. The module-relative copy is the only candidate in every layout.
function launcherIconCandidates() {
const bundled = path.join(__dirname, '..', 'renderer')
return [
path.join(bundled, 'plwl.png'),
path.join(bundled, 'plwl.svg'),
path.join(APP_ROOT, 'renderer', 'plwl.png'),
path.join(APP_ROOT, 'renderer', 'plwl.svg'),
path.join(APP_ROOT, 'olcxja.plwl.png')
]
}
function firstExisting(candidates) {
for (const candidate of candidates) {
try {
if (fs.existsSync(candidate)) return candidate
} catch {}
}
return null
}
let launcherIcon = null
function launcherIconPath() {
if (launcherIcon === null) launcherIcon = firstExisting(launcherIconCandidates()) || ''
return launcherIcon || null
}
// The icon as a real file: a .desktop Icon= cannot live inside app.asar.
function launcherIconDiskPath() {
const onDisk = launcherIconCandidates().filter(candidate => !candidate.includes(`.asar${path.sep}`))
return firstExisting(onDisk)
}
function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8'))
} catch {
return fallback
}
}
function writeJson(file, data) {
fs.mkdirSync(path.dirname(file), { recursive: true })
const tmp = file + '.tmp'
fs.writeFileSync(tmp, JSON.stringify(data, null, 2))
fs.renameSync(tmp, file)
}
module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, launcherIconPath, launcherIconDiskPath, readJson, writeJson, toPortablePath, resolvePortablePath }