Skip to content

Commit 68b8c97

Browse files
committed
Add profile customization and data folder override
1 parent e6554d4 commit 68b8c97

12 files changed

Lines changed: 1329 additions & 43 deletions

electron/bootstrap_paths.cjs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
const { app } = require('electron');
2+
const fs = require('node:fs');
3+
const path = require('node:path');
4+
5+
const APP_NAME = 'lumen';
6+
const BOOTSTRAP_CONFIG_FILENAME = 'bootstrap-paths.json';
7+
8+
function stripTrailingSeparators(inputPath) {
9+
const normalized = path.normalize(String(inputPath || '').trim());
10+
const root = path.parse(normalized).root;
11+
if (!normalized || normalized === root) return normalized;
12+
return normalized.replace(/[\\/]+$/, '');
13+
}
14+
15+
function getDefaultUserDataPath() {
16+
return stripTrailingSeparators(path.join(app.getPath('appData'), APP_NAME));
17+
}
18+
19+
function getBootstrapConfigPath() {
20+
return path.join(getDefaultUserDataPath(), BOOTSTRAP_CONFIG_FILENAME);
21+
}
22+
23+
function normalizeCustomUserDataPath(input) {
24+
const raw = String(input ?? '').trim();
25+
if (!raw) return '';
26+
const resolved = path.resolve(raw);
27+
if (!path.isAbsolute(resolved)) return '';
28+
return stripTrailingSeparators(resolved);
29+
}
30+
31+
function loadBootstrapConfig() {
32+
const fp = getBootstrapConfigPath();
33+
try {
34+
const raw = fs.readFileSync(fp, 'utf8');
35+
const parsed = raw ? JSON.parse(raw) : {};
36+
return parsed && typeof parsed === 'object' ? parsed : {};
37+
} catch {
38+
return {};
39+
}
40+
}
41+
42+
function persistBootstrapConfig(next) {
43+
const fp = getBootstrapConfigPath();
44+
const data = next && typeof next === 'object' ? next : {};
45+
try {
46+
fs.mkdirSync(path.dirname(fp), { recursive: true });
47+
if (!Object.keys(data).length) {
48+
try {
49+
fs.unlinkSync(fp);
50+
} catch {}
51+
return;
52+
}
53+
fs.writeFileSync(fp, JSON.stringify(data, null, 2), 'utf8');
54+
} catch (e) {
55+
return { ok: false, error: String(e && e.message ? e.message : e || 'persist_failed') };
56+
}
57+
return { ok: true };
58+
}
59+
60+
function getBootstrapPathState() {
61+
const defaultUserDataPath = getDefaultUserDataPath();
62+
const disk = loadBootstrapConfig();
63+
let customUserDataPath = normalizeCustomUserDataPath(disk.customUserDataPath);
64+
if (customUserDataPath && customUserDataPath === defaultUserDataPath) {
65+
customUserDataPath = '';
66+
}
67+
return {
68+
bootstrapConfigPath: getBootstrapConfigPath(),
69+
defaultUserDataPath,
70+
customUserDataPath,
71+
usingCustomUserDataPath: !!customUserDataPath,
72+
effectiveUserDataPath: customUserDataPath || defaultUserDataPath,
73+
};
74+
}
75+
76+
function getBootstrapRuntimeState() {
77+
const state = getBootstrapPathState();
78+
let activeUserDataPath = '';
79+
let activeLogsPath = '';
80+
try {
81+
activeUserDataPath = stripTrailingSeparators(app.getPath('userData'));
82+
} catch {}
83+
try {
84+
activeLogsPath = stripTrailingSeparators(app.getPath('logs'));
85+
} catch {}
86+
return {
87+
...state,
88+
activeUserDataPath: activeUserDataPath || state.effectiveUserDataPath,
89+
activeLogsPath,
90+
restartRequired: !!activeUserDataPath && activeUserDataPath !== state.effectiveUserDataPath,
91+
};
92+
}
93+
94+
function ensureUsableDirectory(targetPath) {
95+
const normalized = normalizeCustomUserDataPath(targetPath);
96+
if (!normalized) {
97+
return { ok: false, error: 'invalid_path' };
98+
}
99+
try {
100+
fs.mkdirSync(normalized, { recursive: true });
101+
const stat = fs.statSync(normalized);
102+
if (!stat.isDirectory()) {
103+
return { ok: false, error: 'path_is_not_directory' };
104+
}
105+
fs.accessSync(normalized, fs.constants.R_OK | fs.constants.W_OK);
106+
return { ok: true, path: normalized };
107+
} catch (e) {
108+
return {
109+
ok: false,
110+
error: String(e && e.message ? e.message : e || 'path_unavailable'),
111+
};
112+
}
113+
}
114+
115+
function setCustomUserDataPath(nextPath) {
116+
const normalized = normalizeCustomUserDataPath(nextPath);
117+
if (!normalized) return { ok: false, error: 'invalid_custom_user_data_path' };
118+
119+
const defaultUserDataPath = getDefaultUserDataPath();
120+
if (normalized === defaultUserDataPath) {
121+
return resetCustomUserDataPath();
122+
}
123+
124+
const usability = ensureUsableDirectory(normalized);
125+
if (!usability.ok) {
126+
return { ok: false, error: usability.error || 'path_unavailable' };
127+
}
128+
129+
const saved = persistBootstrapConfig({ customUserDataPath: usability.path });
130+
if (!saved.ok) return saved;
131+
return { ok: true, state: getBootstrapRuntimeState() };
132+
}
133+
134+
function resetCustomUserDataPath() {
135+
const saved = persistBootstrapConfig({});
136+
if (!saved.ok) return saved;
137+
return { ok: true, state: getBootstrapRuntimeState() };
138+
}
139+
140+
function resolveStartupUserDataPath() {
141+
const state = getBootstrapPathState();
142+
const usability = ensureUsableDirectory(state.effectiveUserDataPath);
143+
if (!usability.ok) {
144+
return {
145+
ok: false,
146+
error: usability.error || 'path_unavailable',
147+
state,
148+
};
149+
}
150+
return {
151+
ok: true,
152+
state: {
153+
...state,
154+
effectiveUserDataPath: usability.path,
155+
},
156+
};
157+
}
158+
159+
module.exports = {
160+
APP_NAME,
161+
getBootstrapConfigPath,
162+
getBootstrapRuntimeState,
163+
getBootstrapPathState,
164+
getDefaultUserDataPath,
165+
normalizeCustomUserDataPath,
166+
resolveStartupUserDataPath,
167+
resetCustomUserDataPath,
168+
setCustomUserDataPath,
169+
};

0 commit comments

Comments
 (0)