mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
194 lines
6.7 KiB
JavaScript
194 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Also published as a standalone, bundled installer for devices with Node only.
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { promisify } = require('node:util');
|
|
const execute = promisify(require('node:child_process').execFile);
|
|
const { parseArgs } = require('node:util');
|
|
const artifacts = require('../cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
|
const jobs = require('../cli/src/internal/maintenance/panelUpgrade.cjs');
|
|
|
|
async function installBundle(argv = process.argv.slice(2), env = process.env) {
|
|
const { values } = parseArgs({
|
|
args: argv,
|
|
options: {
|
|
root: { type: 'string' },
|
|
file: { type: 'string' },
|
|
'download-only': { type: 'boolean' },
|
|
install: { type: 'boolean' },
|
|
},
|
|
});
|
|
if (!values.file || !(values.root || env.QL_DIR))
|
|
throw new Error(
|
|
'Usage: node install-bundle.cjs --root /path/to/qinglong --file /path/to/bundle.tar.gz [--download-only | --install]',
|
|
);
|
|
if (values.install && values['download-only'])
|
|
throw new Error('Choose --install or --download-only');
|
|
const root = path.resolve(values.root || env.QL_DIR);
|
|
const existing = await fs.readdir(root).catch((error) => {
|
|
if (error.code !== 'ENOENT') throw error;
|
|
return [];
|
|
});
|
|
if (values.install && existing.length)
|
|
throw new Error('--install requires an empty installation directory');
|
|
if (!values.install) await fs.access(path.join(root, 'shell/share.sh'));
|
|
let environment = {
|
|
...env,
|
|
QL_DIR: root,
|
|
QL_UPGRADE_FILE: path.resolve(values.file),
|
|
};
|
|
if (!values.install) {
|
|
const captured = await execute(
|
|
'bash',
|
|
[
|
|
'--noprofile',
|
|
'--norc',
|
|
'-c',
|
|
'. "$1/shell/share.sh"\n. "$1/shell/env.sh"\nimport_config\nexport QlPort="${ql_port:-${QlPort:-5700}}" QlBaseUrl="${ql_base_url:-${QlBaseUrl:-/}}"\nenv -0',
|
|
'ql-offline',
|
|
root,
|
|
],
|
|
{ env: environment, maxBuffer: 1024 * 1024 },
|
|
);
|
|
const loaded = Object.fromEntries(
|
|
captured.stdout.split('\0').flatMap((line) => {
|
|
const match = /^([A-Za-z_][A-Za-z_0-9]*)=([\s\S]*)$/.exec(line);
|
|
return match ? [[match[1], match[2]]] : [];
|
|
}),
|
|
);
|
|
environment = {
|
|
...environment,
|
|
...loaded,
|
|
QL_DIR: root,
|
|
QL_UPGRADE_FILE: path.resolve(values.file),
|
|
};
|
|
}
|
|
const freshWorkspace = values.install
|
|
? await fs.mkdtemp(path.join(os.tmpdir(), 'ql-offline-install-'))
|
|
: undefined;
|
|
const tmp = freshWorkspace || path.join(root, '.tmp');
|
|
try {
|
|
return await artifacts.withUpgradeLock(
|
|
tmp,
|
|
async () => {
|
|
const selected = await artifacts.stageUpgrade({
|
|
root,
|
|
tmp,
|
|
env: environment,
|
|
run: (program, args, capture) =>
|
|
artifacts.runProcess(environment, program, args, capture),
|
|
onProgress: async (p) => console.log(p.phase),
|
|
install: async () => {
|
|
throw new Error('Offline bundles never install npm dependencies');
|
|
},
|
|
});
|
|
if (values['download-only']) {
|
|
console.log('更新已就绪;执行 ql reload system 应用更新。');
|
|
return selected;
|
|
}
|
|
if (values.install) {
|
|
// Build beside the destination, then atomically replace only an empty root.
|
|
await fs.mkdir(path.dirname(root), { recursive: true });
|
|
const publication = await fs.mkdtemp(
|
|
path.join(path.dirname(root), '.qinglong-install-'),
|
|
);
|
|
try {
|
|
await fs.cp(selected.source, publication, {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
});
|
|
await fs.cp(selected.static, path.join(publication, 'static'), {
|
|
recursive: true,
|
|
});
|
|
await fs.copyFile(
|
|
path.join(publication, '.env.example'),
|
|
path.join(publication, '.env'),
|
|
require('node:fs').constants.COPYFILE_EXCL,
|
|
);
|
|
await fs.chmod(path.join(publication, '.env'), 0o600);
|
|
await fs.rename(publication, root);
|
|
} finally {
|
|
await fs.rm(publication, { recursive: true, force: true });
|
|
}
|
|
await fs.rm(path.dirname(selected.source), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
await fs.rm(
|
|
path.join(
|
|
tmp,
|
|
`upgrade-ready-${artifacts.releaseBranch(environment)}.json`,
|
|
),
|
|
{ force: true },
|
|
);
|
|
console.log(
|
|
`离线安装完成,保留现有 Node ${
|
|
process.version
|
|
}。启动服务:QL_DIR=${JSON.stringify(root)} bash ${JSON.stringify(
|
|
path.join(root, 'shell/start-simplify.sh'),
|
|
)}`,
|
|
);
|
|
return;
|
|
}
|
|
return jobs.withPanelLifecycle(
|
|
root,
|
|
tmp,
|
|
environment,
|
|
async (hook, prepareSelection) => {
|
|
const expected = await jobs.stagedMetadata(selected);
|
|
const previous = await jobs
|
|
.readRunningBuild(environment)
|
|
.catch(() => undefined);
|
|
const selection = await prepareSelection?.(selected);
|
|
try {
|
|
return await artifacts.reloadSystem({
|
|
root,
|
|
tmp,
|
|
env: environment,
|
|
data: environment.dir_data || path.join(root, 'data'),
|
|
staticRoot: path.join(root, 'static'),
|
|
config:
|
|
environment.dir_config || path.join(root, 'data/config'),
|
|
lifecycle: {
|
|
stop: () => hook('stop'),
|
|
start: async (recover) => {
|
|
await hook('start');
|
|
await artifacts.waitForHealth(environment);
|
|
if (!recover)
|
|
await jobs.verifyRunningVersion(
|
|
environment,
|
|
expected,
|
|
previous,
|
|
);
|
|
},
|
|
},
|
|
additionalReplacements: selection
|
|
? [{ source: selection.source, target: selection.target }]
|
|
: [],
|
|
});
|
|
} finally {
|
|
if (selection)
|
|
await fs.rm(selection.directory, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
},
|
|
environment,
|
|
);
|
|
} finally {
|
|
if (freshWorkspace)
|
|
await fs.rm(freshWorkspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
module.exports = { installBundle };
|
|
if (require.main === module)
|
|
installBundle().catch((error) => {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
});
|