mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
317365c766
Introduce bump-version.js with CI workflows for patch bumps on dev and stable releases on main, plus panel and installer UI to choose stable vs development GitHub update branches.
240 lines
7.9 KiB
JavaScript
240 lines
7.9 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Merge .env files for BetterDesk updates (issue #158).
|
|
* On update: preserve every existing key; append only missing keys from template.
|
|
* On fresh install: write full template (after substitution).
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
|
|
/** Keys never overwritten when explicitly merging with overwritePaths (unused on update). */
|
|
const PROTECTED_KEYS = new Set([
|
|
'SESSION_SECRET',
|
|
'DEFAULT_ADMIN_PASSWORD',
|
|
'DEFAULT_ADMIN_USERNAME',
|
|
'DATABASE_URL',
|
|
'DB_TYPE',
|
|
'DB_PATH'
|
|
]);
|
|
|
|
/**
|
|
* Parse .env content into ordered entries: { type: 'comment'|'blank'|'pair', key?, value?, raw? }
|
|
*/
|
|
function parseEnvLines(content) {
|
|
const lines = String(content || '').split(/\r?\n/);
|
|
const entries = [];
|
|
const map = new Map();
|
|
|
|
for (const raw of lines) {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
entries.push({ type: 'blank', raw });
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith('#')) {
|
|
entries.push({ type: 'comment', raw });
|
|
continue;
|
|
}
|
|
const eq = raw.indexOf('=');
|
|
if (eq === -1) {
|
|
entries.push({ type: 'comment', raw });
|
|
continue;
|
|
}
|
|
const key = raw.slice(0, eq).trim();
|
|
const value = raw.slice(eq + 1);
|
|
if (!key) {
|
|
entries.push({ type: 'comment', raw });
|
|
continue;
|
|
}
|
|
entries.push({ type: 'pair', key, value, raw });
|
|
map.set(key, value);
|
|
}
|
|
return { entries, map };
|
|
}
|
|
|
|
/**
|
|
* Apply __PLACEHOLDER__ substitutions to template text.
|
|
* @param {string} text
|
|
* @param {Record<string, string>} subs
|
|
*/
|
|
function applySubstitutions(text, subs = {}) {
|
|
let out = text;
|
|
for (const [key, val] of Object.entries(subs)) {
|
|
const token = `__${key}__`;
|
|
out = out.split(token).join(String(val ?? ''));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Build substitution map for .env.example placeholders from existing .env and/or runtime config.
|
|
* @param {object} [opts]
|
|
* @param {string} [opts.existingContent]
|
|
* @param {Map<string,string>} [opts.envMap]
|
|
* @param {object} [opts.config] - BetterDesk config module export
|
|
*/
|
|
function buildEnvSubstitutions(opts = {}) {
|
|
const envMap = opts.envMap || parseEnvLines(opts.existingContent || '').map;
|
|
const config = opts.config || {};
|
|
|
|
const pick = (key, ...fallbacks) => {
|
|
const fromEnv = envMap.get(key);
|
|
if (fromEnv !== undefined && fromEnv !== '') return fromEnv;
|
|
for (const fb of fallbacks) {
|
|
if (fb !== undefined && fb !== null && fb !== '') return String(fb);
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const rustdeskDir = pick('RUSTDESK_DIR', config.rustdeskDir, config.keysPath);
|
|
const goPort = pick('GO_API_PORT', config.goApiPort, '21114');
|
|
const apiPort = pick('API_PORT', config.apiPort, '21121');
|
|
const dbType = pick('DB_TYPE', config.dbType, 'sqlite');
|
|
const sslCert = pick('SSL_CERT_PATH', config.sslCertPath, rustdeskDir ? `${rustdeskDir}/ssl/betterdesk.crt` : '');
|
|
const sslKey = pick('SSL_KEY_PATH', config.sslKeyPath, rustdeskDir ? `${rustdeskDir}/ssl/betterdesk.key` : '');
|
|
|
|
const goApiBase = `http://localhost:${goPort}/api`;
|
|
|
|
return {
|
|
RUSTDESK_DIR: rustdeskDir,
|
|
PUB_KEY_PATH: pick('PUB_KEY_PATH', config.pubKeyPath, rustdeskDir ? `${rustdeskDir}/id_ed25519.pub` : ''),
|
|
API_KEY_PATH: pick('API_KEY_PATH', config.apiKeyPath, rustdeskDir ? `${rustdeskDir}/.api_key` : ''),
|
|
DB_TYPE: dbType,
|
|
DB_PATH: pick('DB_PATH', config.dbPath, rustdeskDir ? `${rustdeskDir}/db_v2.sqlite3` : ''),
|
|
DATABASE_URL: pick('DATABASE_URL', config.databaseUrl, ''),
|
|
DATA_DIR: pick('DATA_DIR', config.dataDir, ''),
|
|
GO_API_PORT: String(goPort),
|
|
HBBS_API_URL: pick('HBBS_API_URL', goApiBase),
|
|
BETTERDESK_API_URL: pick('BETTERDESK_API_URL', goApiBase),
|
|
API_PORT: String(apiPort),
|
|
DEFAULT_ADMIN_PASSWORD: pick('DEFAULT_ADMIN_PASSWORD', ''),
|
|
SESSION_SECRET: pick('SESSION_SECRET', config.sessionSecret, ''),
|
|
SSL_CERT_PATH: sslCert,
|
|
SSL_KEY_PATH: sslKey
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reject template values that still contain unresolved __PLACEHOLDER__ tokens.
|
|
*/
|
|
function assertResolvedSubstitutions(subs) {
|
|
for (const [key, val] of Object.entries(subs)) {
|
|
if (/__[^_]+__/.test(String(val))) {
|
|
throw new Error(`Unresolved .env substitution for ${key}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge template into existing .env (update mode).
|
|
* Existing keys are never changed; missing keys are appended from template.
|
|
* @param {string} existingContent
|
|
* @param {string} templateContent
|
|
* @returns {{ content: string, added: string[] }}
|
|
*/
|
|
function mergeEnv(existingContent, templateContent) {
|
|
const existing = parseEnvLines(existingContent);
|
|
const template = parseEnvLines(templateContent);
|
|
const added = [];
|
|
|
|
let content = existingContent;
|
|
if (!content.endsWith('\n') && content.length) content += '\n';
|
|
|
|
let pendingComment = '';
|
|
for (const entry of template.entries) {
|
|
if (entry.type === 'comment') {
|
|
pendingComment += entry.raw + '\n';
|
|
continue;
|
|
}
|
|
if (entry.type === 'blank') {
|
|
pendingComment += entry.raw + '\n';
|
|
continue;
|
|
}
|
|
if (existing.map.has(entry.key)) continue;
|
|
|
|
if (pendingComment) {
|
|
content += pendingComment;
|
|
pendingComment = '';
|
|
}
|
|
content += `${entry.key}=${entry.value}\n`;
|
|
added.push(entry.key);
|
|
}
|
|
|
|
return { content, added };
|
|
}
|
|
|
|
/**
|
|
* Build full .env from template (fresh install).
|
|
*/
|
|
function buildFreshEnv(templateContent) {
|
|
return templateContent.endsWith('\n') ? templateContent : templateContent + '\n';
|
|
}
|
|
|
|
/**
|
|
* Upsert a single key in .env content (preserve comments and order).
|
|
*/
|
|
function upsertEnvKey(existingContent, key, value) {
|
|
const safeValue = String(value ?? '');
|
|
const lines = String(existingContent || '').split(/\r?\n/);
|
|
let replaced = false;
|
|
const out = lines.map((line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) return line;
|
|
const eq = line.indexOf('=');
|
|
if (eq <= 0) return line;
|
|
const lineKey = line.slice(0, eq).trim();
|
|
if (lineKey !== key) return line;
|
|
replaced = true;
|
|
return `${key}=${safeValue}`;
|
|
});
|
|
let content = out.join('\n');
|
|
if (!replaced) {
|
|
if (content.length && !content.endsWith('\n')) content += '\n';
|
|
content += `${key}=${safeValue}\n`;
|
|
} else if (!content.endsWith('\n')) {
|
|
content += '\n';
|
|
}
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Read template, apply substitutions, merge or replace target file.
|
|
* @param {object} opts
|
|
* @param {string} opts.targetPath - path to .env
|
|
* @param {string} opts.templatePath - path to .env.example
|
|
* @param {boolean} [opts.freshInstall=false]
|
|
* @param {Record<string, string>} [opts.substitutions={}]
|
|
*/
|
|
function mergeEnvFile(opts) {
|
|
const { targetPath, templatePath, freshInstall = false, substitutions = {} } = opts;
|
|
if (!fs.existsSync(templatePath)) {
|
|
throw new Error(`Template not found: ${templatePath}`);
|
|
}
|
|
assertResolvedSubstitutions(substitutions);
|
|
const templateRaw = applySubstitutions(fs.readFileSync(templatePath, 'utf8'), substitutions);
|
|
const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, 'utf8') : '';
|
|
|
|
if (freshInstall || !existing.trim()) {
|
|
const content = buildFreshEnv(templateRaw);
|
|
fs.writeFileSync(targetPath, content, { mode: 0o600 });
|
|
return { mode: 'fresh', added: [...parseEnvLines(templateRaw).map.keys()] };
|
|
}
|
|
|
|
const { content, added } = mergeEnv(existing, templateRaw);
|
|
fs.writeFileSync(targetPath, content, { mode: 0o600 });
|
|
return { mode: 'merge', added };
|
|
}
|
|
|
|
module.exports = {
|
|
PROTECTED_KEYS,
|
|
parseEnvLines,
|
|
applySubstitutions,
|
|
buildEnvSubstitutions,
|
|
assertResolvedSubstitutions,
|
|
mergeEnv,
|
|
buildFreshEnv,
|
|
upsertEnvKey,
|
|
mergeEnvFile
|
|
};
|