mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-04 00:42:01 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
301efa9e72 | ||
|
|
5eaff6ba90 | ||
|
|
855f591992 | ||
|
|
d30eb2008c | ||
|
|
e1ce0f3fa9 | ||
|
|
82514e65e1 | ||
|
|
31261190f0 |
@@ -21,6 +21,3 @@ JWT_EXPIRES_IN=
|
||||
QINIU_AK=
|
||||
QINIU_SK=
|
||||
QINIU_SCOPE=
|
||||
|
||||
# Python 虚拟环境路径,留空则使用系统 Python
|
||||
# PYTHON_VENV_DIR=./.venv
|
||||
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
run: |
|
||||
docker run --rm --network none --entrypoint node \
|
||||
qinglong-readiness:${{ matrix.image }} \
|
||||
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.4.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
||||
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.3.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
||||
- name: Export native Debian image for isolated dev validation
|
||||
if: matrix.image == 'debian'
|
||||
run: |
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Node.js 原生部署与 Python venv
|
||||
|
||||
设置 `QL_DIR` 为安装目录,`QL_DATA_DIR` 为以 `/data` 结尾的绝对持久化数据路径。
|
||||
如需隔离 Python,请在启动进程的环境中显式设置 `PYTHON_VENV_DIR`(绝对路径或相对 `QL_DIR` 的路径),例如 `export PYTHON_VENV_DIR=/srv/qinglong/.venv`。未设置时保留现有 Python prefix 行为。
|
||||
|
||||
- `qinglong`(`shell/start.sh`)安装系统依赖和 Node 工具,并安装 Python requests,然后调用公共启动脚本。
|
||||
- `npm run start:simple`(`shell/start-simplify.sh`)使用已准备的 Node、pm2、nginx、jq 等依赖,不安装系统包、全局 npm 包或 requests;显式指定的 venv 不存在时仍会创建。请先在 venv 中安装 requests 和任务所需依赖。
|
||||
- 两种启动方式均保留已有 `.env`;使用 venv 时清除继承的 `PYTHONPATH`/`PYTHONUSERBASE`,依赖查询、安装和卸载使用该 venv 的 Python。
|
||||
|
||||
该模式需具备现有启动流程所要求的 nginx 配置和 pm2 服务权限。生产环境设置变量应通过 systemd 的 `Environment` 或相应进程配置完成。已有系统 Python 依赖不自动迁移至 venv,需在 venv 中重新安装。
|
||||
+23
-2
@@ -35,6 +35,23 @@ function isPathAllowed(targetPath: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
async function writeScriptFile(
|
||||
targetPath: string,
|
||||
content: string,
|
||||
): Promise<boolean> {
|
||||
const target = resolve(targetPath);
|
||||
for (const writableRoot of config.writePathList) {
|
||||
const root = resolve(writableRoot);
|
||||
const prefix = root.endsWith(sep) ? root : root + sep;
|
||||
// Check the actual write target, including filename, at the write boundary.
|
||||
if (!target.startsWith(prefix)) continue;
|
||||
if (!isPathAllowed(target)) return false;
|
||||
await writeFileWithLock(target, content);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function existingWritableRealPath(targetPath: string): Promise<string> {
|
||||
if (!isPathAllowed(targetPath)) {
|
||||
throw new ScriptHistoryError(403, '暂无权限');
|
||||
@@ -373,7 +390,9 @@ export default (app: Router) => {
|
||||
await rmPath(originFilePath);
|
||||
}
|
||||
}
|
||||
await writeFileWithLock(filePath, content);
|
||||
if (!(await writeScriptFile(filePath, content))) {
|
||||
return res.send({ code: 403, message: t('暂无权限') });
|
||||
}
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return historyError(e, res, next);
|
||||
@@ -513,7 +532,9 @@ export default (app: Router) => {
|
||||
if (!isPathAllowed(filePath)) {
|
||||
return res.send({ code: 403, message: t('暂无权限') });
|
||||
}
|
||||
await writeFileWithLock(filePath, content || '');
|
||||
if (!(await writeScriptFile(filePath, content || ''))) {
|
||||
return res.send({ code: 403, message: t('暂无权限') });
|
||||
}
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.runScript(filePath);
|
||||
|
||||
@@ -27,7 +27,6 @@ export const SAMPLE_FILES = [
|
||||
];
|
||||
|
||||
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
|
||||
export const PYTHON_VENV_DIR = process.env.PYTHON_VENV_DIR;
|
||||
|
||||
export const NotificationModeStringMap = {
|
||||
0: 'gotify',
|
||||
|
||||
+102
-196
@@ -1,25 +1,18 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { statSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import psTreeFun from 'ps-tree';
|
||||
import { promisify } from 'util';
|
||||
import { load } from 'js-yaml';
|
||||
import config from './index';
|
||||
import { PYTHON_INSTALL_DIR, PYTHON_VENV_DIR, TASK_COMMAND } from './const';
|
||||
import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';
|
||||
import Logger from '../loaders/logger';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
import { DependenceTypes } from '../data/dependence';
|
||||
import { FormData } from 'undici';
|
||||
import os from 'os';
|
||||
import { maybeSudo, isInContainer } from './container';
|
||||
import { shellQuote } from '../shared/shellQuote';
|
||||
import { resolveFileAccess } from '../shared/fileAccess';
|
||||
import {
|
||||
mirrorWriteCommand,
|
||||
normalizeMirror,
|
||||
rewriteMirrorSources,
|
||||
} from '../shared/linuxMirror';
|
||||
|
||||
export * from './share';
|
||||
|
||||
@@ -677,9 +670,7 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
||||
throw new Error('Invalid timezone');
|
||||
}
|
||||
|
||||
await promiseExec(
|
||||
maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`),
|
||||
);
|
||||
await promiseExec(maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`));
|
||||
await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`);
|
||||
|
||||
return true;
|
||||
@@ -689,150 +680,61 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a name is a GitHub URL
|
||||
function getPythonExecutable(): string {
|
||||
return PYTHON_VENV_DIR
|
||||
? shellQuote(path.resolve(config.rootPath, PYTHON_VENV_DIR, 'bin/python3'))
|
||||
: 'python3';
|
||||
}
|
||||
|
||||
function getPythonPip(): string {
|
||||
return PYTHON_VENV_DIR ? `${getPythonExecutable()} -m pip` : 'pip3';
|
||||
}
|
||||
|
||||
function isGitHubUrl(name: string): boolean {
|
||||
// Support git+https://, git+http://, https://, and http:// URLs
|
||||
// This covers GitHub URLs and other git-compatible repositories
|
||||
return !!name.match(/^(git\+https?:\/\/|https?:\/\/)/i);
|
||||
}
|
||||
|
||||
// Helper function to check if a name is a requirements file
|
||||
function isRequirementsFile(name: string): boolean {
|
||||
return !!name.match(/requirements.*\.(txt|in)$/i);
|
||||
}
|
||||
|
||||
// Helper function to check if a name is a pyproject.toml file
|
||||
function isPyprojectToml(name: string): boolean {
|
||||
return name.endsWith('pyproject.toml');
|
||||
}
|
||||
|
||||
export function getGetCommand(type: DependenceTypes, name: string): string {
|
||||
const trimmedName = name.trim();
|
||||
|
||||
// For Python dependencies installed from GitHub or requirements files,
|
||||
// we can't reliably check if they're installed, so skip the check
|
||||
if (type === DependenceTypes.python3) {
|
||||
if (
|
||||
isGitHubUrl(trimmedName) ||
|
||||
isRequirementsFile(trimmedName) ||
|
||||
isPyprojectToml(trimmedName)
|
||||
) {
|
||||
// Return a command that will always indicate not installed
|
||||
// This ensures GitHub URLs and requirements files are always installed
|
||||
return 'echo ""';
|
||||
}
|
||||
}
|
||||
|
||||
const baseCommands = {
|
||||
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`,
|
||||
[DependenceTypes.python3]: `${getPythonExecutable()} -c ${shellQuote(
|
||||
`from importlib.metadata import version\nimport importlib.util as u\nname=${JSON.stringify(
|
||||
trimmedName,
|
||||
)}\ntry:\n print(version(name))\nexcept Exception:\n print(name if u.find_spec(name) else '')`,
|
||||
)}`,
|
||||
[DependenceTypes.linux]:
|
||||
getOsTypeSync() === 'Alpine'
|
||||
? `apk info -es ${trimmedName}`
|
||||
: maybeSudo(`dpkg-query -s ${trimmedName}`),
|
||||
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
|
||||
[DependenceTypes.python3]: `
|
||||
python3 -c "exec('''
|
||||
name='${name}'
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
print(version(name))
|
||||
except:
|
||||
import importlib.util as u
|
||||
import importlib.metadata as m
|
||||
spec=u.find_spec(name)
|
||||
print(name if spec else '')
|
||||
''')"`,
|
||||
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
|
||||
? `apk info -es ${name}`
|
||||
: maybeSudo(`dpkg-query -s ${name}`),
|
||||
};
|
||||
|
||||
return baseCommands[type];
|
||||
}
|
||||
|
||||
export function getInstallCommand(type: DependenceTypes, name: string): string {
|
||||
if (type === DependenceTypes.python3) {
|
||||
const prefix =
|
||||
PYTHON_INSTALL_DIR && !PYTHON_VENV_DIR
|
||||
? ` --prefix=${shellQuote(PYTHON_INSTALL_DIR)}`
|
||||
: '';
|
||||
const pip = `${getPythonPip()} install --disable-pip-version-check${
|
||||
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||
}${prefix}`;
|
||||
name = name.trim();
|
||||
if (isRequirementsFile(name) || isPyprojectToml(name)) {
|
||||
const file = [config.scriptPath, config.repoPath]
|
||||
.filter(Boolean)
|
||||
.map((root) => resolveFileAccess(root, [name]))
|
||||
.find((file) => {
|
||||
if (!file) return false;
|
||||
try {
|
||||
return statSync(file).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!file)
|
||||
throw new Error(
|
||||
'Python dependency file must be inside scripts or repo',
|
||||
);
|
||||
return isRequirementsFile(name)
|
||||
? `${pip} -r ${shellQuote(file)}`
|
||||
: `${pip} ${shellQuote(path.dirname(file))}`;
|
||||
}
|
||||
return `${pip} -- ${shellQuote(name)}`;
|
||||
}
|
||||
const baseCommands = {
|
||||
[DependenceTypes.nodejs]: 'pnpm add -g',
|
||||
[DependenceTypes.linux]:
|
||||
getOsTypeSync() === 'Alpine'
|
||||
? 'apk add --no-check-certificate'
|
||||
: maybeSudo('apt-get install -y'),
|
||||
[DependenceTypes.python3]:
|
||||
'pip3 install --disable-pip-version-check --root-user-action=ignore',
|
||||
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
|
||||
? 'apk add --no-check-certificate'
|
||||
: maybeSudo('apt-get install -y'),
|
||||
};
|
||||
return `${baseCommands[type]} ${name.trim()}`;
|
||||
|
||||
let command = baseCommands[type];
|
||||
|
||||
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
|
||||
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
|
||||
}
|
||||
|
||||
return `${command} ${name.trim()}`;
|
||||
}
|
||||
|
||||
export function getUninstallCommand(
|
||||
type: DependenceTypes,
|
||||
name: string,
|
||||
): string {
|
||||
if (type === DependenceTypes.python3) {
|
||||
name = name.trim();
|
||||
if (isRequirementsFile(name)) {
|
||||
const file = [config.scriptPath, config.repoPath]
|
||||
.filter(Boolean)
|
||||
.map((root) => resolveFileAccess(root, [name]))
|
||||
.find((file) => {
|
||||
if (!file) return false;
|
||||
try {
|
||||
return statSync(file).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!file)
|
||||
throw new Error(
|
||||
'Python dependency file must be inside scripts or repo',
|
||||
);
|
||||
return `${getPythonPip()} uninstall --disable-pip-version-check${
|
||||
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||
} -y -r ${shellQuote(file)}`;
|
||||
}
|
||||
if (isPyprojectToml(name) || isGitHubUrl(name)) {
|
||||
// A source URL/project can install several distributions and has no safe
|
||||
// inverse. Removing its panel entry leaves installed packages intact.
|
||||
return `printf '%s\\n' ${shellQuote(
|
||||
'Remove installed Python distributions separately by package name.',
|
||||
)}`;
|
||||
}
|
||||
return `${getPythonPip()} uninstall --disable-pip-version-check${
|
||||
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
|
||||
} -y -- ${shellQuote(name)}`;
|
||||
}
|
||||
const baseCommands = {
|
||||
[DependenceTypes.nodejs]: 'pnpm remove -g',
|
||||
[DependenceTypes.linux]:
|
||||
getOsTypeSync() === 'Alpine' ? 'apk del' : maybeSudo('apt-get remove -y'),
|
||||
[DependenceTypes.python3]:
|
||||
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
|
||||
[DependenceTypes.linux]: getOsTypeSync() === 'Alpine'
|
||||
? 'apk del'
|
||||
: maybeSudo('apt-get remove -y'),
|
||||
};
|
||||
|
||||
return `${baseCommands[type]} ${name.trim()}`;
|
||||
}
|
||||
|
||||
@@ -904,73 +806,77 @@ export async function detectOS(): Promise<
|
||||
return osType;
|
||||
}
|
||||
|
||||
async function getCurrentMirrorDomain(
|
||||
filePath: string,
|
||||
): Promise<string | null> {
|
||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||
const lines = fileContent.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const match = line.match(/https?:\/\/[^\/]+/);
|
||||
if (match) {
|
||||
return match[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function replaceDomainInFile(
|
||||
filePath: string,
|
||||
oldDomainWithScheme: string,
|
||||
newDomainWithScheme: string,
|
||||
): Promise<void> {
|
||||
let fileContent = await fs.readFile(filePath, 'utf8');
|
||||
let updatedContent = fileContent.replace(
|
||||
new RegExp(oldDomainWithScheme, 'g'),
|
||||
newDomainWithScheme,
|
||||
);
|
||||
|
||||
if (!newDomainWithScheme.endsWith('/')) {
|
||||
newDomainWithScheme += '/';
|
||||
}
|
||||
|
||||
await writeFileWithLock(filePath, updatedContent);
|
||||
}
|
||||
|
||||
async function _updateLinuxMirror(
|
||||
osType: string,
|
||||
mirror: string,
|
||||
mirrorDomainWithScheme: string,
|
||||
): Promise<string> {
|
||||
const defaults: Record<string, string> = {
|
||||
Debian: 'http://deb.debian.org/debian',
|
||||
Ubuntu: 'http://archive.ubuntu.com/ubuntu',
|
||||
Alpine: 'http://dl-cdn.alpinelinux.org/alpine',
|
||||
};
|
||||
if (!defaults[osType])
|
||||
throw new Error('Unsupported OS type for updating mirrors.');
|
||||
const target = mirror || defaults[osType];
|
||||
normalizeMirror(target);
|
||||
const S = isInContainer() ? 'sudo ' : '';
|
||||
const update =
|
||||
osType === 'Alpine' ? 'apk update' : `${S}apt-get update --error-on=any`;
|
||||
let file =
|
||||
osType === 'Alpine'
|
||||
? '/etc/apk/repositories'
|
||||
: `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
|
||||
let content = '';
|
||||
try {
|
||||
content = await fs.readFile(file, 'utf8');
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
if (osType !== 'Alpine') {
|
||||
try {
|
||||
file = '/etc/apt/sources.list';
|
||||
content = await fs.readFile(file, 'utf8');
|
||||
} catch (legacyError: any) {
|
||||
if (legacyError.code !== 'ENOENT') throw legacyError;
|
||||
file = `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
|
||||
// APT otherwise returns zero when index downloads fail with transient errors.
|
||||
const aptUpdate = `${S}apt-get update --error-on=any`;
|
||||
let filePath: string, currentDomainWithScheme: string | null;
|
||||
switch (osType) {
|
||||
case 'Debian':
|
||||
filePath = '/etc/apt/sources.list.d/debian.sources';
|
||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||
if (currentDomainWithScheme) {
|
||||
return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://deb.debian.org'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath}) && ${aptUpdate}`;
|
||||
} else {
|
||||
return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath} && ${aptUpdate}`;
|
||||
}
|
||||
}
|
||||
case 'Ubuntu':
|
||||
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
|
||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||
if (currentDomainWithScheme) {
|
||||
return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath}) && ${aptUpdate}`;
|
||||
} else {
|
||||
return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath} && ${aptUpdate}`;
|
||||
}
|
||||
case 'Alpine':
|
||||
filePath = '/etc/apk/repositories';
|
||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||
if (currentDomainWithScheme) {
|
||||
return `sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org'}|g' ${filePath} || (mkdir -p /etc/apk && echo -e "\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/main\\n\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/community" | sed "s|^|${mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org'}/alpine/v|" | tee ${filePath}) && apk update`;
|
||||
} else {
|
||||
return `mkdir -p /etc/apk && echo -e "\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/main\\n\\$(grep VERSION_ID /etc/os-release | cut -d= -f2 | cut -d. -f1,2)/community" | sed "s|^|${mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org'}/alpine/v|" | tee ${filePath} && apk update`;
|
||||
}
|
||||
default:
|
||||
throw Error('Unsupported OS type for updating mirrors.');
|
||||
}
|
||||
if (!content.trim()) {
|
||||
const release = await fs.readFile('/etc/os-release', 'utf8');
|
||||
if (osType === 'Alpine') {
|
||||
const version = release.match(/^VERSION_ID=["']?(\d+\.\d+)/m)?.[1];
|
||||
if (!version)
|
||||
throw new Error('Cannot determine Alpine repository version');
|
||||
content = `${defaults.Alpine}/v${version}/main\n${defaults.Alpine}/v${version}/community\n`;
|
||||
} else {
|
||||
// The fallback may have read an empty legacy .list. Deb822 stanzas must
|
||||
// be written to a .sources file, leaving the legacy file untouched.
|
||||
file = `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
|
||||
const codename = release.match(/^VERSION_CODENAME=["']?([\w-]+)/m)?.[1];
|
||||
if (!codename)
|
||||
throw new Error('Cannot determine APT repository codename');
|
||||
const ubuntu = osType === 'Ubuntu';
|
||||
content = `Types: deb\nURIs: ${
|
||||
defaults[osType]
|
||||
}\nSuites: ${codename} ${codename}-updates${
|
||||
ubuntu ? ` ${codename}-backports` : ''
|
||||
}\nComponents: ${
|
||||
ubuntu ? 'main restricted universe multiverse' : 'main'
|
||||
}\nSigned-By: /usr/share/keyrings/${osType.toLowerCase()}-archive-keyring.gpg\n`;
|
||||
if (!ubuntu)
|
||||
content += `\nTypes: deb\nURIs: http://security.debian.org/debian-security\nSuites: ${codename}-security\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n`;
|
||||
}
|
||||
}
|
||||
return mirrorWriteCommand(
|
||||
file,
|
||||
rewriteMirrorSources(content, target),
|
||||
S,
|
||||
update,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
|
||||
|
||||
@@ -179,14 +179,7 @@ export default ({ app }: { app: Application }) => {
|
||||
app.use(rewrite('/open/*', '/api/$1'));
|
||||
app.use(config.api.prefix, routes());
|
||||
|
||||
app.get('*', (req, res, next) => {
|
||||
// Don't serve index.html for API routes
|
||||
if (
|
||||
req.path === config.api.prefix ||
|
||||
req.path.startsWith(`${config.api.prefix}/`)
|
||||
) {
|
||||
return res.status(404).json({ code: 404, message: 'Not Found' });
|
||||
}
|
||||
app.get('*', (_, res, next) => {
|
||||
const indexPath = path.join(frontendPath, 'index.html');
|
||||
res.sendFile(indexPath, (err) => {
|
||||
if (err) {
|
||||
|
||||
+12
-34
@@ -32,11 +32,10 @@ export default class DependenceService {
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async create(payloads: Dependence[]): Promise<Dependence[]> {
|
||||
const tabs = payloads.map((x) => {
|
||||
getInstallCommand(x.type, x.name);
|
||||
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
|
||||
return tab;
|
||||
});
|
||||
@@ -60,7 +59,6 @@ export default class DependenceService {
|
||||
...other,
|
||||
status: DependenceStatus.queued,
|
||||
});
|
||||
getInstallCommand(tab.type, tab.name);
|
||||
const newDoc = await this.updateDb(tab);
|
||||
this.installDependenceOneByOne([newDoc]);
|
||||
return newDoc;
|
||||
@@ -298,23 +296,6 @@ export default class DependenceService {
|
||||
force: boolean = false,
|
||||
) {
|
||||
return taskLimit.runDependeny(dependency, () => {
|
||||
let validatedCommand: string;
|
||||
try {
|
||||
validatedCommand = isInstall
|
||||
? getInstallCommand(dependency.type, dependency.name.trim())
|
||||
: getUninstallCommand(dependency.type, dependency.name.trim());
|
||||
} catch (error) {
|
||||
taskLimit.removeQueuedDependency(dependency);
|
||||
return DependenceModel.update(
|
||||
{
|
||||
status: isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed,
|
||||
log: [String(error)],
|
||||
},
|
||||
{ where: { id: dependency.id } },
|
||||
).then(() => null);
|
||||
}
|
||||
return new Promise(async (resolve) => {
|
||||
if (taskLimit.firstDependencyId !== dependency.id) {
|
||||
return resolve(null);
|
||||
@@ -364,7 +345,9 @@ export default class DependenceService {
|
||||
: DependenceStatus.removing;
|
||||
await DependenceModel.update({ status }, { where: { id: depIds } });
|
||||
|
||||
let command = validatedCommand;
|
||||
let command = isInstall
|
||||
? getInstallCommand(dependency.type, depName)
|
||||
: getUninstallCommand(dependency.type, depName);
|
||||
if (isLinuxDependence) {
|
||||
command = isInstall
|
||||
? `${linuxCommand.install} ${depName.trim()}`
|
||||
@@ -487,20 +470,15 @@ export default class DependenceService {
|
||||
|
||||
const message =
|
||||
'\n' +
|
||||
tf(
|
||||
'依赖%s%s,结束时间 %s,耗时 %s 秒',
|
||||
actionText,
|
||||
resultText,
|
||||
endTime.format('YYYY-MM-DD HH:mm:ss'),
|
||||
String(endTime.diff(startTime, 'second')),
|
||||
);
|
||||
tf('依赖%s%s,结束时间 %s,耗时 %s 秒',
|
||||
actionText,
|
||||
resultText,
|
||||
endTime.format('YYYY-MM-DD HH:mm:ss'),
|
||||
String(endTime.diff(startTime, 'second')),
|
||||
);
|
||||
const exitStatus = isSucceed
|
||||
? isInstall
|
||||
? DependenceStatus.installed
|
||||
: DependenceStatus.removed
|
||||
: isInstall
|
||||
? DependenceStatus.installFailed
|
||||
: DependenceStatus.removeFailed;
|
||||
? (isInstall ? DependenceStatus.installed : DependenceStatus.removed)
|
||||
: (isInstall ? DependenceStatus.installFailed : DependenceStatus.removeFailed);
|
||||
this.sockService.sendMessage({
|
||||
type: socketMessageType,
|
||||
message,
|
||||
|
||||
+28
-12
@@ -12,7 +12,7 @@ export class HttpServerService {
|
||||
async initialize(expressApp: express.Application, port: number) {
|
||||
const hostsToTry = [
|
||||
config.bindHost,
|
||||
...(config.bindHost === '::' ? ['0.0.0.0'] : [])
|
||||
...(config.bindHost === '::' ? ['0.0.0.0'] : []),
|
||||
];
|
||||
|
||||
let lastError: Error | null = null;
|
||||
@@ -20,16 +20,21 @@ export class HttpServerService {
|
||||
for (const host of hostsToTry) {
|
||||
try {
|
||||
const server = await this.tryListen(expressApp, port, host);
|
||||
Logger.debug(`[boot] HTTP service started successfully on ${host}:${port}`);
|
||||
Logger.debug(
|
||||
`[boot] HTTP service started successfully on ${host}:${port}`,
|
||||
);
|
||||
metricsService.record('http_service_start', 1, {
|
||||
port: port.toString(),
|
||||
host
|
||||
host,
|
||||
});
|
||||
this.server = server;
|
||||
return server;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
Logger.warn(`Failed to bind HTTP on ${host}:${port}, trying next...`, err);
|
||||
Logger.warn(
|
||||
`Failed to bind HTTP on ${host}:${port}, trying next...`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,17 +42,28 @@ export class HttpServerService {
|
||||
throw lastError || new Error('Failed to start HTTP service');
|
||||
}
|
||||
|
||||
private async tryListen(expressApp: express.Application, port: number, host: string): Promise<Server> {
|
||||
private async tryListen(
|
||||
expressApp: express.Application,
|
||||
port: number,
|
||||
host: string,
|
||||
): Promise<Server> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// There is one HTTP worker; accepting here avoids primary IPC handoff
|
||||
// for every connection. Restore shared listening for custom clusters.
|
||||
const server = expressApp.listen({
|
||||
port,
|
||||
host,
|
||||
exclusive: process.env.QL_HTTP_SHARED_LISTEN !== 'true',
|
||||
}, () => {
|
||||
resolve(server);
|
||||
});
|
||||
const server = expressApp.listen(
|
||||
{
|
||||
port,
|
||||
host,
|
||||
exclusive: process.env.QL_HTTP_SHARED_LISTEN !== 'true',
|
||||
},
|
||||
() => {
|
||||
resolve(server);
|
||||
},
|
||||
);
|
||||
|
||||
server.requestTimeout = 300000;
|
||||
server.headersTimeout = 120000;
|
||||
server.keepAliveTimeout = 65000;
|
||||
|
||||
server.on('error', (err: Error) => {
|
||||
server.close();
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { shellQuote } from './shellQuote';
|
||||
|
||||
const repositoryNames =
|
||||
/^(debian(?:-security)?|ubuntu(?:-ports)?|alpine|armbian)$/;
|
||||
|
||||
export function normalizeMirror(mirror: string): URL {
|
||||
if (/\s/.test(mirror)) throw new Error('Invalid Linux mirror URL');
|
||||
const url = new URL(mirror);
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.search ||
|
||||
url.hash
|
||||
) {
|
||||
throw new Error(
|
||||
'Linux mirror must be an HTTP(S) URL without credentials, query or fragment',
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function replaceMirrorUrl(current: string, mirror: string): string {
|
||||
const target = normalizeMirror(mirror);
|
||||
const previous = new URL(current);
|
||||
const oldSegments = previous.pathname.split('/').filter(Boolean);
|
||||
const newSegments = target.pathname.split('/').filter(Boolean);
|
||||
const repositoryIndex = oldSegments.findIndex((part) =>
|
||||
repositoryNames.test(part),
|
||||
);
|
||||
const targetIndex = newSegments.findIndex((part) =>
|
||||
repositoryNames.test(part),
|
||||
);
|
||||
const prefix =
|
||||
targetIndex >= 0 ? newSegments.slice(0, targetIndex) : newSegments;
|
||||
if (repositoryIndex >= 0) {
|
||||
const repository = oldSegments[repositoryIndex];
|
||||
let suffix = oldSegments.slice(repositoryIndex + 1);
|
||||
// Repair paths already corrupted by repeated authority-only replacement.
|
||||
while (suffix[0] === repository) suffix.shift();
|
||||
return `${target.origin}/${[...prefix, repository, ...suffix].join('/')}`;
|
||||
}
|
||||
// Unknown repository paths: a full URL replaces the old root, a hostname
|
||||
// preserves it. Never append the newly entered root to the existing root.
|
||||
return `${target.origin}/${(newSegments.length
|
||||
? newSegments
|
||||
: oldSegments
|
||||
).join('/')}`;
|
||||
}
|
||||
|
||||
export function rewriteMirrorSources(content: string, mirror: string): string {
|
||||
normalizeMirror(mirror);
|
||||
return content
|
||||
.split(/(\r?\n[ \t]*\r?\n)/)
|
||||
.map((stanza) => {
|
||||
const signedBy = stanza.match(/^Signed-By:\s*([^\r\n]*)/im)?.[1];
|
||||
return stanza
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (!/^\s*(?:URIs:|deb(?:-src)?\s|https?:\/\/)/.test(line))
|
||||
return line;
|
||||
const legacy = /^\s*deb(?:-src)?\s/.test(line);
|
||||
const key = legacy
|
||||
? line.match(/\bsigned-by=([^\]\s]+)/i)?.[1]
|
||||
: signedBy;
|
||||
// In .list files a # starts an inline comment; its URLs are metadata.
|
||||
const comment = legacy ? line.indexOf('#') : -1;
|
||||
const source = comment < 0 ? line : line.slice(0, comment);
|
||||
const rewritten = source.replace(/https?:\/\/[^\s#]+/g, (url) =>
|
||||
isSystemRepository(url, key) ? replaceMirrorUrl(url, mirror) : url,
|
||||
);
|
||||
return rewritten + (comment < 0 ? '' : line.slice(comment));
|
||||
})
|
||||
.join('\n');
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function isSystemRepository(url: string, signedBy?: string): boolean {
|
||||
const keys =
|
||||
signedBy
|
||||
?.trim()
|
||||
.split(/[\s,]+/)
|
||||
.filter(Boolean) || [];
|
||||
const archiveKeyring =
|
||||
keys.length > 0 &&
|
||||
keys.every((key) =>
|
||||
/\/(?:debian|ubuntu|armbian)-archive-keyring\.(?:gpg|asc)$/.test(key),
|
||||
);
|
||||
// A separate signing key identifies a third-party archive, even if its
|
||||
// repository happens to be called /debian or /ubuntu.
|
||||
if (keys.length > 0 && !archiveKeyring) return false;
|
||||
const segments = new URL(url).pathname.split('/').filter(Boolean);
|
||||
const repositoryIndex = segments.findIndex((part) =>
|
||||
repositoryNames.test(part),
|
||||
);
|
||||
if (repositoryIndex < 0) return false;
|
||||
// Without archive metadata, accept only conventional archive roots. A
|
||||
// vendor path such as Docker's /linux/debian is not the Debian archive.
|
||||
return (
|
||||
archiveKeyring ||
|
||||
repositoryIndex === 0 ||
|
||||
segments
|
||||
.slice(0, repositoryIndex)
|
||||
.every((part) => /^mirrors?$/.test(part)) ||
|
||||
segments[repositoryIndex] === 'alpine'
|
||||
);
|
||||
}
|
||||
|
||||
export function mirrorWriteCommand(
|
||||
file: string,
|
||||
content: string,
|
||||
prefix: string,
|
||||
update: string,
|
||||
): string {
|
||||
return `${prefix}mkdir -p ${shellQuote(
|
||||
file.slice(0, file.lastIndexOf('/')),
|
||||
)} && printf '%s' ${shellQuote(content)} | ${prefix}tee ${shellQuote(
|
||||
file,
|
||||
)} >/dev/null && ${update}`;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export async function writeFileWithLock(
|
||||
}
|
||||
if (!(await fileExist(filePath))) {
|
||||
const fileHandle = await open(filePath, 'w');
|
||||
fileHandle.close();
|
||||
await fileHandle.close();
|
||||
}
|
||||
const lockfilePath = getUniqueLockPath(filePath);
|
||||
|
||||
@@ -34,9 +34,12 @@ export async function writeFileWithLock(
|
||||
},
|
||||
lockfilePath,
|
||||
});
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
try {
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
}
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
await release();
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ test('cron metadata precedence agrees with unchanged legacy add_cron', (t) => {
|
||||
'--noprofile',
|
||||
'--norc',
|
||||
'-c',
|
||||
`${definition}\nt() { :; }\nnotify_api() { :; }\nadd_cron_api() { printf '%s:%s:%s:%s' "$1" "$2" "$3" "$4"; }\nadd_cron "$dir_scripts/list" repo`,
|
||||
`${definition}\nt() { :; }\nnotify_api() { :; }\nadd_cron_api() { printf '%s' "$1"; }\nadd_cron "$dir_scripts/list" repo`,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Python 依赖来源
|
||||
|
||||
依赖管理中可手动添加 Python 包名、`git+https://` 等 pip 支持的源 URL,或脚本/订阅仓库目录内的 requirements 文件、pyproject.toml 路径。文件路径支持目录空格,必须位于持久化 scripts 或 repo 根目录内;越界与指向根目录外的符号链接会被拒绝。
|
||||
|
||||
requirements 使用 `pip install -r`,pyproject 使用完整项目目录安装,保留相对依赖文件和项目源码。拉取订阅不会自动安装仓库声明的依赖。
|
||||
|
||||
删除 requirements 条目会调用 `pip uninstall -r`。源 URL 或 pyproject 条目无法可靠逆向推断其安装的所有分发包,因此删除该条目只移除面板记录;需另外按包名卸载已安装的包,日志会明确提示。
|
||||
+3
-4
@@ -28,8 +28,7 @@
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
"start:simple": "bash shell/start-simplify.sh"
|
||||
"postinstall": "max setup 2>/dev/null || true"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
@@ -109,9 +108,9 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"keyv": "^5.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "2.4.0",
|
||||
"multer": "2.3.0",
|
||||
"node-cron": "4.6.0",
|
||||
"nodemailer": "^10.0.9",
|
||||
"nodemailer": "^8.0.1",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"ps-tree": "^1.2.0",
|
||||
|
||||
Generated
+20
-354
@@ -28,7 +28,7 @@ dependencies:
|
||||
version: 12.0.1
|
||||
body-parser:
|
||||
specifier: ^1.20.3
|
||||
version: 1.20.6
|
||||
version: 1.20.3
|
||||
celebrate:
|
||||
specifier: ^15.0.3
|
||||
version: 15.0.3
|
||||
@@ -73,7 +73,7 @@ dependencies:
|
||||
version: 1.2.0
|
||||
http-proxy-middleware:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.7
|
||||
version: 3.0.3
|
||||
iconv-lite:
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3
|
||||
@@ -82,7 +82,7 @@ dependencies:
|
||||
version: 2.3.0(@types/node@17.0.45)
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.3.2
|
||||
version: 4.1.0
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
@@ -93,14 +93,14 @@ dependencies:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
multer:
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
node-cron:
|
||||
specifier: 4.6.0
|
||||
version: 4.6.0
|
||||
nodemailer:
|
||||
specifier: ^10.0.9
|
||||
version: 10.0.9
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
p-queue-cjs:
|
||||
specifier: 7.3.4
|
||||
version: 7.3.4
|
||||
@@ -133,7 +133,7 @@ dependencies:
|
||||
version: 0.10.0
|
||||
undici:
|
||||
specifier: ^7.9.0
|
||||
version: 7.29.1
|
||||
version: 7.9.0
|
||||
uuid:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
@@ -264,7 +264,7 @@ devDependencies:
|
||||
version: 4.23.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
|
||||
axios:
|
||||
specifier: ^1.4.0
|
||||
version: 1.20.0
|
||||
version: 1.7.8
|
||||
compression-webpack-plugin:
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0
|
||||
@@ -2879,7 +2879,7 @@ packages:
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
import-fresh: 3.3.0
|
||||
js-yaml: 4.3.2
|
||||
js-yaml: 4.1.0
|
||||
minimatch: 3.1.5
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
@@ -3060,7 +3060,7 @@ packages:
|
||||
camelcase: 5.3.1
|
||||
find-up: 4.1.0
|
||||
get-package-type: 0.1.0
|
||||
js-yaml: 3.15.2
|
||||
js-yaml: 3.14.1
|
||||
resolve-from: 5.0.0
|
||||
dev: true
|
||||
|
||||
@@ -5677,6 +5677,7 @@ packages:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/agentkeepalive@4.5.0:
|
||||
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||
@@ -8301,7 +8302,7 @@ packages:
|
||||
is-glob: 4.0.3
|
||||
is-path-inside: 3.0.3
|
||||
js-sdsl: 4.4.2
|
||||
js-yaml: 4.3.2
|
||||
js-yaml: 4.1.0
|
||||
json-stable-stringify-without-jsonify: 1.0.1
|
||||
levn: 0.4.1
|
||||
lodash.merge: 4.6.2
|
||||
@@ -8512,7 +8513,7 @@ packages:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
body-parser: 1.20.6
|
||||
body-parser: 1.20.3
|
||||
content-disposition: 0.5.4
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.1
|
||||
@@ -9333,6 +9334,7 @@ packages:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
@@ -10585,7 +10587,7 @@ packages:
|
||||
engines: {node: '>=8.6'}
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.2
|
||||
picomatch: 2.3.1
|
||||
|
||||
/miller-rabin@4.0.1:
|
||||
resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==}
|
||||
@@ -10652,19 +10654,19 @@ packages:
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
brace-expansion: 1.1.21
|
||||
brace-expansion: 1.1.12
|
||||
dev: true
|
||||
|
||||
/minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
dependencies:
|
||||
brace-expansion: 1.1.21
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
/minimatch@9.0.9:
|
||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
brace-expansion: 2.1.7
|
||||
brace-expansion: 2.0.2
|
||||
dev: true
|
||||
|
||||
/minimist-options@4.1.0:
|
||||
@@ -10868,7 +10870,7 @@ packages:
|
||||
semver: 7.7.4
|
||||
tar: 7.5.20
|
||||
tinyglobby: 0.2.17
|
||||
undici: 6.29.0
|
||||
undici: 6.27.0
|
||||
which: 6.0.1
|
||||
dev: false
|
||||
optional: true
|
||||
@@ -15792,342 +15794,6 @@ packages:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
dev: true
|
||||
|
||||
/brace-expansion@2.1.7:
|
||||
resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
dev: true
|
||||
|
||||
|
||||
/brace-expansion@1.1.21:
|
||||
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
|
||||
/axios@1.20.0:
|
||||
resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==}
|
||||
dependencies:
|
||||
follow-redirects: 1.16.1(debug@4.4.3)
|
||||
form-data: 4.0.6
|
||||
https-proxy-agent: 5.0.1
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
||||
/proxy-from-env@2.1.0:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
|
||||
/form-data@4.0.6:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.4
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
|
||||
/hasown@2.0.4:
|
||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
|
||||
/es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.4
|
||||
dev: true
|
||||
|
||||
|
||||
/get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.2
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.4
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
|
||||
/math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
|
||||
/gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
|
||||
/get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.2
|
||||
|
||||
|
||||
/es-object-atoms@1.1.2:
|
||||
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
|
||||
/dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
|
||||
/call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
|
||||
/es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
|
||||
/follow-redirects@1.16.1(debug@4.4.3):
|
||||
resolution: {integrity: sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
|
||||
|
||||
/undici@7.29.1:
|
||||
resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
dev: false
|
||||
|
||||
|
||||
/undici@6.29.0:
|
||||
resolution: {integrity: sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==}
|
||||
engines: {node: '>=18.17'}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
|
||||
/nodemailer@10.0.9:
|
||||
resolution: {integrity: sha512-BF0qcyplCwp+jMk6HCjFykBz/YhhZSsxrARhOldLwFWH+8kGjQsd2WIMIZhqEuyXRoGFi0ONbDeWDMDoL8MhLw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
dev: false
|
||||
|
||||
|
||||
/multer@2.4.0:
|
||||
resolution: {integrity: sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==}
|
||||
engines: {node: '>= 10.16.0'}
|
||||
dependencies:
|
||||
append-field: 1.0.0
|
||||
busboy: 1.6.0
|
||||
type-is: 1.6.18
|
||||
dev: false
|
||||
|
||||
|
||||
/js-yaml@4.3.2:
|
||||
resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
|
||||
/js-yaml@3.15.2:
|
||||
resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
dev: true
|
||||
|
||||
|
||||
/http-proxy-middleware@3.0.7:
|
||||
resolution: {integrity: sha512-iwbQltVlx8bCrqePUM8C+hllHvdawVhQJaLrj1X7qllkvFQdXFsr16pW/mo9+JDVjN+QO2XUx9jd8SmoFkE5qw==}
|
||||
engines: {node: ^14.18.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@types/http-proxy': 1.17.17
|
||||
debug: 4.4.3
|
||||
http-proxy: 1.18.1(debug@4.4.3)
|
||||
is-glob: 4.0.3
|
||||
is-plain-object: 5.1.0
|
||||
micromatch: 4.0.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
|
||||
/picomatch@2.3.2:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
|
||||
/is-plain-object@5.1.0:
|
||||
resolution: {integrity: sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
|
||||
/http-proxy@1.18.1(debug@4.4.3):
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.16.1(debug@4.4.3)
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
|
||||
/@types/http-proxy@1.17.17:
|
||||
resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==}
|
||||
dependencies:
|
||||
'@types/node': 17.0.45
|
||||
dev: false
|
||||
|
||||
|
||||
/body-parser@1.20.6:
|
||||
resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.4.24
|
||||
on-finished: 2.4.1
|
||||
qs: 6.15.3
|
||||
raw-body: 2.5.3
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
|
||||
/raw-body@2.5.3:
|
||||
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
|
||||
/http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.2
|
||||
toidentifier: 1.0.1
|
||||
|
||||
|
||||
/statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
|
||||
/qs@6.15.3:
|
||||
resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
|
||||
engines: {node: '>=0.6'}
|
||||
dependencies:
|
||||
es-define-property: 1.0.1
|
||||
side-channel: 1.1.1
|
||||
dev: false
|
||||
|
||||
|
||||
/side-channel@1.1.1:
|
||||
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-list: 1.0.1
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
|
||||
/side-channel-weakmap@1.0.2:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
|
||||
/side-channel-map@1.0.1:
|
||||
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
|
||||
/object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
|
||||
/call-bound@1.0.4:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
|
||||
/side-channel-list@1.0.1:
|
||||
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
+3
-14
@@ -85,13 +85,9 @@ add_cron_api() {
|
||||
sub_id="null"
|
||||
fi
|
||||
|
||||
local payload
|
||||
payload=$(jq -cn --arg name "$name" --arg command "$command" \
|
||||
--arg schedule "$schedule" --argjson sub_id "$sub_id" \
|
||||
'{name: $name, command: $command, schedule: $schedule, sub_id: $sub_id}') || return $?
|
||||
local api=$(ql_api_request \
|
||||
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||
--data-raw "$payload" \
|
||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
|
||||
-X POST)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
message=$(echo "$api" | jq -r .message)
|
||||
@@ -116,14 +112,10 @@ update_cron_api() {
|
||||
local id="$4"
|
||||
fi
|
||||
|
||||
local payload
|
||||
payload=$(jq -cn --arg name "$name" --arg command "$command" \
|
||||
--arg schedule "$schedule" --arg id "$id" \
|
||||
'{name: $name, command: $command, schedule: $schedule, id: $id}') || return $?
|
||||
local api=$(ql_api_request \
|
||||
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||
-X PUT \
|
||||
--data-raw "$payload")
|
||||
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}")
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
message=$(echo "$api" | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
@@ -143,13 +135,10 @@ update_cron_command_api() {
|
||||
local id="$2"
|
||||
fi
|
||||
|
||||
local payload
|
||||
payload=$(jq -cn --arg command "$command" --arg id "$id" \
|
||||
'{command: $command, id: $id}') || return $?
|
||||
local api=$(ql_api_request \
|
||||
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
|
||||
-X PUT \
|
||||
--data-raw "$payload")
|
||||
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}")
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
message=$(echo "$api" | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
: "${QL_DIR:?Please set QL_DIR}"
|
||||
: "${QL_DATA_DIR:?Please set QL_DATA_DIR}"
|
||||
[[ "$QL_DIR" = /* && "$QL_DATA_DIR" = /*/data ]] || { echo "QL_DIR and QL_DATA_DIR must be absolute paths (data directory ends in /data)" >&2; exit 1; }
|
||||
command="$1"
|
||||
|
||||
export PNPM_HOME="${PNPM_HOME:-${QL_DATA_DIR}/dep_cache/node}"
|
||||
if [[ -n "${PYTHON_VENV_DIR:-}" ]]; then
|
||||
[[ "$PYTHON_VENV_DIR" = /* ]] || PYTHON_VENV_DIR="${QL_DIR}/${PYTHON_VENV_DIR}"
|
||||
export PYTHON_VENV_DIR
|
||||
if [[ ! -x "$PYTHON_VENV_DIR/bin/python3" ]]; then
|
||||
python3 -m venv "$PYTHON_VENV_DIR"
|
||||
fi
|
||||
[[ -x "$PYTHON_VENV_DIR/bin/python3" ]] || { echo "Invalid Python venv" >&2; exit 1; }
|
||||
unset PYTHON_HOME PYTHONUSERBASE PYTHONPATH
|
||||
export PATH="${PYTHON_VENV_DIR}/bin:${PATH}:${PNPM_HOME}"
|
||||
export PIP_CACHE_DIR="${PYTHON_VENV_DIR}/pip"
|
||||
else
|
||||
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
export PYTHON_HOME="${PYTHON_HOME:-${QL_DATA_DIR}/dep_cache/python3}"
|
||||
export PYTHONUSERBASE="$PYTHON_HOME"
|
||||
export PATH="${PATH}:${PNPM_HOME}:${PYTHON_HOME}/bin"
|
||||
export PIP_CACHE_DIR="${PYTHON_HOME}/pip"
|
||||
export PYTHONPATH="${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages"
|
||||
fi
|
||||
export NODE_PATH="${NODE_PATH:-/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules}"
|
||||
mkdir -p "$PIP_CACHE_DIR"
|
||||
|
||||
cd "${QL_DIR}"
|
||||
if [[ ! -e .env ]]; then
|
||||
# Preserve user configuration, including a file created concurrently.
|
||||
(umask 077; set -C; cat .env.example > .env)
|
||||
fi
|
||||
chmod u+x "${QL_DIR}"/shell/*.sh
|
||||
|
||||
. "${QL_DIR}/shell/share.sh"
|
||||
. "${QL_DIR}/shell/env.sh"
|
||||
|
||||
log_with_style() {
|
||||
local level="$1"
|
||||
local message="$2"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
|
||||
}
|
||||
|
||||
log_with_style "INFO" "🚀 1. 检测配置文件..."
|
||||
import_config "$@"
|
||||
make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
fix_config
|
||||
|
||||
pm2 l &>/dev/null
|
||||
|
||||
log_with_style "INFO" "🔄 2. 启动 nginx..."
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
|
||||
log_with_style "INFO" "⚙️ 3. 启动 pm2 服务..."
|
||||
reload_pm2
|
||||
|
||||
if [[ $command != "reload" ]]; then
|
||||
if [[ $AutoStartBot == true ]]; then
|
||||
log_with_style "INFO" "🤖 4. 启动 bot..."
|
||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
|
||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||
fi
|
||||
|
||||
pm2 startup
|
||||
pm2 save
|
||||
fi
|
||||
|
||||
log_with_style "SUCCESS" "🎉 启动成功!"
|
||||
+61
-9
@@ -77,13 +77,65 @@ if [[ $command != "reload" ]]; then
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5
|
||||
fi
|
||||
|
||||
if [[ "$command" != "reload" ]]; then
|
||||
if [[ -n "${PYTHON_VENV_DIR:-}" ]]; then
|
||||
[[ "$PYTHON_VENV_DIR" = /* ]] || export PYTHON_VENV_DIR="${QL_DIR}/${PYTHON_VENV_DIR}"
|
||||
[[ -x "$PYTHON_VENV_DIR/bin/python3" ]] || python3 -m venv "$PYTHON_VENV_DIR"
|
||||
"$PYTHON_VENV_DIR/bin/python3" -m pip install requests
|
||||
else
|
||||
pip3 install --prefix "${PYTHON_HOME:-${QL_DATA_DIR}/dep_cache/python3}" requests
|
||||
fi
|
||||
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
export PNPM_HOME=${QL_DIR}/data/dep_cache/node
|
||||
export PYTHON_HOME=${QL_DIR}/data/dep_cache/python3
|
||||
export PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin
|
||||
export NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules
|
||||
export PIP_CACHE_DIR=${PYTHON_HOME}/pip
|
||||
export PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||
|
||||
if [[ $command != "reload" ]]; then
|
||||
pip3 install --prefix ${PYTHON_HOME} requests
|
||||
fi
|
||||
exec /bin/bash "${QL_DIR}/shell/start-simplify.sh" "$@"
|
||||
|
||||
cd "${QL_DIR}"
|
||||
if [[ ! -e .env ]]; then
|
||||
# Preserve user configuration, including a file created concurrently.
|
||||
(umask 077; set -C; cat .env.example > .env)
|
||||
fi
|
||||
chmod 777 ${QL_DIR}/shell/*.sh
|
||||
|
||||
. ${QL_DIR}/shell/share.sh
|
||||
. ${QL_DIR}/shell/env.sh
|
||||
|
||||
log_with_style() {
|
||||
local level="$1"
|
||||
local message="$2"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
|
||||
}
|
||||
|
||||
log_with_style "INFO" "🚀 1. 检测配置文件..."
|
||||
import_config "$@"
|
||||
make_dir /etc/nginx/conf.d
|
||||
make_dir /run/nginx
|
||||
fix_config
|
||||
|
||||
pm2 l &>/dev/null
|
||||
|
||||
log_with_style "INFO" "🔄 2. 启动 nginx..."
|
||||
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
|
||||
|
||||
log_with_style "INFO" "⚙️ 3. 启动 pm2 服务..."
|
||||
reload_pm2
|
||||
|
||||
if [[ $command != "reload" ]]; then
|
||||
if [[ $AutoStartBot == true ]]; then
|
||||
log_with_style "INFO" "🤖 4. 启动 bot..."
|
||||
nohup ql bot >$dir_log/bot.log 2>&1 &
|
||||
fi
|
||||
|
||||
if [[ $EnableExtraShell == true ]]; then
|
||||
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
|
||||
nohup ql extra >$dir_log/extra.log 2>&1 &
|
||||
fi
|
||||
|
||||
pm2 startup
|
||||
pm2 save
|
||||
fi
|
||||
|
||||
log_with_style "SUCCESS" "🎉 启动成功!"
|
||||
|
||||
+2
-6
@@ -99,9 +99,7 @@ add_cron() {
|
||||
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
|
||||
[[ -z $cron_name ]] && cron_name=$(grep "name:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_name ]] && cron_name=$(basename "$file_name")
|
||||
cron_name=${cron_name//$'\r'/}
|
||||
cron_line=${cron_line//$'\r'/}
|
||||
result=$(add_cron_api "$cron_line" "${cmd_task} ${file}" "$cron_name" "$SUB_ID")
|
||||
result=$(add_cron_api "${cron_line}:${cmd_task} ${file}:${cron_name}:${SUB_ID}")
|
||||
echo -e "$result"
|
||||
if [[ $detail ]]; then
|
||||
detail="${detail}${result}\n"
|
||||
@@ -192,9 +190,7 @@ update_raw() {
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
|
||||
[[ -z $cron_line ]] && cron_line="$default_cron"
|
||||
cron_name=${cron_name//$'\r'/}
|
||||
cron_line=${cron_line//$'\r'/}
|
||||
result=$(add_cron_api "$cron_line" "${cmd_task} ${filename}" "$cron_name" "$SUB_ID")
|
||||
result=$(add_cron_api "${cron_line}:${cmd_task} ${filename}:${cron_name}:${SUB_ID}")
|
||||
echo -e "$result\n"
|
||||
notify_api "新增任务通知" "\n$result"
|
||||
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
|
||||
|
||||
@@ -395,7 +395,7 @@
|
||||
"父目录": "Parent Directory",
|
||||
"版本": "Version",
|
||||
"状态": "Status",
|
||||
"环境变量": "Environment variable",
|
||||
"环境变量": "Environment Variables",
|
||||
"生成数据中...": "Generating data...",
|
||||
"用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories",
|
||||
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "Token, which can be obtained after scanning and logging in, is available under 'My Account' -> 'Settings' -> 'Tokens'. Please refer to the instructions for detailed steps: https://www.weplusbot.com/",
|
||||
@@ -634,6 +634,7 @@
|
||||
"保存配置": "Save",
|
||||
"当前生效值": "Effective value",
|
||||
"默认配置": "Default",
|
||||
"环境变量": "Environment variable",
|
||||
"客户端 IP 诊断": "Client IP diagnostics",
|
||||
"重新诊断": "Run again",
|
||||
"Socket 地址": "Socket address",
|
||||
@@ -698,7 +699,5 @@
|
||||
"仍然保存": "Save anyway",
|
||||
"已保存,本次未记录历史版本": "Saved without a history snapshot",
|
||||
"已保存,但旧历史清理失败,请检查存储权限和空间": "Saved, but old history could not be removed. Check storage permissions and free space.",
|
||||
"保存失败": "Unable to save",
|
||||
"Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python supports package names, Git URLs, and existing requirements.txt or pyproject.toml files under the scripts or repository directory.",
|
||||
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "Package name, Git URL, requirements.txt or pyproject.toml path"
|
||||
"保存失败": "Unable to save"
|
||||
}
|
||||
|
||||
@@ -634,6 +634,7 @@
|
||||
"保存配置": "保存配置",
|
||||
"当前生效值": "当前生效值",
|
||||
"默认配置": "默认配置",
|
||||
"环境变量": "环境变量",
|
||||
"客户端 IP 诊断": "客户端 IP 诊断",
|
||||
"重新诊断": "重新诊断",
|
||||
"Socket 地址": "Socket 地址",
|
||||
@@ -698,7 +699,5 @@
|
||||
"仍然保存": "仍然保存",
|
||||
"已保存,本次未记录历史版本": "已保存,本次未记录历史版本",
|
||||
"已保存,但旧历史清理失败,请检查存储权限和空间": "已保存,但旧历史清理失败,请检查存储权限和空间",
|
||||
"保存失败": "保存失败",
|
||||
"Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径",
|
||||
"支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "支持包名、GitHub链接、requirements.txt或pyproject.toml路径"
|
||||
"保存失败": "保存失败"
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ const DependenceModal = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState(
|
||||
DependenceTypes[defaultType as any],
|
||||
);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
@@ -93,7 +90,7 @@ const DependenceModal = ({
|
||||
label={intl.get('依赖类型')}
|
||||
initialValue={DependenceTypes[defaultType as any]}
|
||||
>
|
||||
<Select onChange={(value) => setSelectedType(value)}>
|
||||
<Select>
|
||||
{config.dependenceTypes.map((x, i) => (
|
||||
<Option key={i} value={i}>
|
||||
{x}
|
||||
@@ -124,24 +121,11 @@ const DependenceModal = ({
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
tooltip={
|
||||
selectedType === DependenceTypes.python3
|
||||
? intl.get(
|
||||
'Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径',
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={
|
||||
selectedType === DependenceTypes.python3
|
||||
? intl.get(
|
||||
'支持包名、GitHub链接、requirements.txt或pyproject.toml路径',
|
||||
)
|
||||
: intl.get('请输入依赖名称')
|
||||
}
|
||||
placeholder={intl.get('请输入依赖名称')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label={intl.get('备注')}>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
const test = require('node:test'),
|
||||
assert = require('node:assert/strict'),
|
||||
fs = require('node:fs'),
|
||||
os = require('node:os'),
|
||||
path = require('node:path'),
|
||||
express = require('express');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
test('unknown API requests return JSON 404 while env.js and SPA deep links work', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-fallback-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(dir, 'static/dist'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'static/dist/index.html'), 'SPA');
|
||||
const app = express();
|
||||
load('back/loaders/express.ts', {
|
||||
'../api': () => express.Router(),
|
||||
'../config': {
|
||||
rootPath: dir,
|
||||
uploadPath: dir,
|
||||
api: { prefix: '/api' },
|
||||
apiWhiteList: ['/api/env.js'],
|
||||
jwt: { secret: 'test' },
|
||||
},
|
||||
'express-jwt': {
|
||||
expressjwt: () => ({ unless: () => (_q, _r, next) => next() }),
|
||||
UnauthorizedError: class extends Error {},
|
||||
},
|
||||
'../config/util': {
|
||||
getPlatform: () => 'desktop',
|
||||
getToken: () => 'stored',
|
||||
},
|
||||
'../config/serverEnv': {
|
||||
serveEnv: (_q, r) => r.type('js').send('window.QL_CONFIG={};'),
|
||||
},
|
||||
'../shared/store': {
|
||||
shareStore: { getAuthInfo: async () => ({}), getApps: async () => [] },
|
||||
},
|
||||
'../shared/auth': {
|
||||
isValidToken: () => true,
|
||||
isDefaultAuthInfo: () => false,
|
||||
},
|
||||
'../shared/i18n': { t: (s) => s },
|
||||
'../middlewares/uniqueConstraintError': (_q, _r, next) => next(),
|
||||
'../middlewares/protectedPathCase': (_q, _r, next) => next(),
|
||||
'../data/system': {},
|
||||
'../data/open': {},
|
||||
}).default({ app });
|
||||
const server = await new Promise((r) => {
|
||||
const s = app.listen(0, '127.0.0.1', () => r(s));
|
||||
});
|
||||
t.after(() => new Promise((r) => server.close(r)));
|
||||
const url = `http://127.0.0.1:${server.address().port}`;
|
||||
for (const p of ['/api', '/api/missing', '/open/missing']) {
|
||||
const res = await fetch(url + p);
|
||||
assert.equal(res.status, 404, p);
|
||||
assert.match(res.headers.get('content-type'), /json/);
|
||||
assert.equal((await res.json()).code, 404);
|
||||
}
|
||||
const env = await fetch(url + '/api/env.js');
|
||||
assert.match(await env.text(), /QL_CONFIG/);
|
||||
assert.equal(await (await fetch(url + '/script')).text(), 'SPA');
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
const test = require('node:test'),
|
||||
assert = require('node:assert/strict'),
|
||||
fs = require('node:fs'),
|
||||
os = require('node:os'),
|
||||
path = require('node:path');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
const { writeFileWithLock, getUniqueLockPath } = load('back/shared/utils.ts', {
|
||||
'../config/util': { fileExist: async (p) => fs.existsSync(p) },
|
||||
});
|
||||
test('write failure releases the lock and preserves the original error', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-lock-review-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
const p = path.join(dir, 'script');
|
||||
fs.writeFileSync(p, 'original');
|
||||
await assert.rejects(
|
||||
writeFileWithLock(p, 'bad', { encoding: 'invalid-encoding' }),
|
||||
(e) => e.code === 'ERR_INVALID_ARG_VALUE',
|
||||
);
|
||||
assert.equal(fs.existsSync(getUniqueLockPath(p)), false);
|
||||
await writeFileWithLock(p, 'retry');
|
||||
assert.equal(fs.readFileSync(p, 'utf8'), 'retry');
|
||||
});
|
||||
test('first write closes its file handle and keeps requested permissions', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-lock-review-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
const p = path.join(dir, 'new');
|
||||
await writeFileWithLock(p, 'created', { mode: 0o600 });
|
||||
assert.equal(fs.readFileSync(p, 'utf8'), 'created');
|
||||
assert.equal(fs.statSync(p).mode & 0o777, 0o600);
|
||||
});
|
||||
@@ -1,253 +0,0 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
const hasApt = spawnSync('apt-get', ['--version']).status === 0;
|
||||
const {
|
||||
rewriteMirrorSources,
|
||||
replaceMirrorUrl,
|
||||
normalizeMirror,
|
||||
mirrorWriteCommand,
|
||||
} = require('../../back/shared/linuxMirror');
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-issue-fixes-'));
|
||||
t.after(() => fs.rm(root, { force: true, recursive: true }));
|
||||
return root;
|
||||
}
|
||||
|
||||
test('#2902/#2871 mirror writes are idempotent, preserve suites, security/ports/version paths and quote URLs', async (t) => {
|
||||
const original = `# http://leave.example/debian\nTypes: deb\nURIs: http://deb.debian.org/debian\nSuites: bookworm bookworm-updates\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n\nTypes: deb\nURIs: http://security.debian.org/debian-security\nSuites: bookworm-security\n`;
|
||||
const url = 'https://mirror.example/mirrors/debian/';
|
||||
const changed = rewriteMirrorSources(original, url);
|
||||
assert.equal(rewriteMirrorSources(changed, url), changed);
|
||||
assert.match(
|
||||
changed,
|
||||
/URIs: https:\/\/mirror.example\/mirrors\/debian-security/,
|
||||
);
|
||||
assert.match(changed, /Suites: bookworm-security/);
|
||||
assert.match(changed, /# http:\/\/leave.example\/debian/);
|
||||
assert.equal(
|
||||
replaceMirrorUrl('http://old/debian/debian/debian', url),
|
||||
'https://mirror.example/mirrors/debian',
|
||||
);
|
||||
assert.equal(
|
||||
replaceMirrorUrl('http://old/armbian/armbian', 'https://new/armbian'),
|
||||
'https://new/armbian',
|
||||
);
|
||||
assert.equal(
|
||||
replaceMirrorUrl('http://old/ubuntu-ports', 'https://new/ubuntu'),
|
||||
'https://new/ubuntu-ports',
|
||||
);
|
||||
assert.equal(
|
||||
replaceMirrorUrl('http://old/alpine/v3.22/main', 'https://new/alpine'),
|
||||
'https://new/alpine/v3.22/main',
|
||||
);
|
||||
assert.equal(
|
||||
replaceMirrorUrl('http://old/debian', 'https://new'),
|
||||
'https://new/debian',
|
||||
);
|
||||
for (const bad of [
|
||||
'file:///tmp/a',
|
||||
'http://u:p@host',
|
||||
'https://host?x=1',
|
||||
'https://host/\nanything',
|
||||
])
|
||||
assert.throws(() => normalizeMirror(bad));
|
||||
const root = await fixture(t);
|
||||
const file = path.join(root, "quote's", 'sources');
|
||||
const command = mirrorWriteCommand(file, changed, '', 'true');
|
||||
const result = spawnSync('bash', ['-c', command], { encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(await fs.readFile(file, 'utf8'), changed);
|
||||
});
|
||||
|
||||
test('#2701 shell API serializes CR/LF, quotes, backslashes and colons without corrupting JSON', async () => {
|
||||
const result = spawnSync(
|
||||
'bash',
|
||||
[
|
||||
'-c',
|
||||
`source shell/api.sh
|
||||
ql_api_request() { while [[ $# -gt 0 ]]; do if [[ "$1" == --data-raw ]]; then printf '%s\\n' "$2" >&3; break; fi; shift; done; printf '{"code":200}'; }
|
||||
t() { :; }
|
||||
add_cron_api '0 0 1 1 *' 'task a:b.js' $'quote"slash\\\\colon:\\r\\n\\t中文' 23
|
||||
update_cron_api '0 0 1 1 *' 'task a:b.js' $'quote"slash\\\\colon:\\r\\n\\t中文' 27
|
||||
update_cron_command_api $'task path\\\\with"quotes.js' 27
|
||||
`,
|
||||
],
|
||||
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe', 'pipe'] },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const payloads = result.output[3].trim().split('\n').map(JSON.parse);
|
||||
assert.equal(payloads[0].name, 'quote"slash\\colon:\r\n\t中文');
|
||||
assert.equal(payloads[0].sub_id, 23);
|
||||
assert.equal(payloads[1].id, '27');
|
||||
assert.equal(payloads[2].command, 'task path\\with"quotes.js');
|
||||
});
|
||||
|
||||
for (const distribution of ['Debian', 'Ubuntu']) {
|
||||
for (const legacyContent of ['', ' \t\r\n']) {
|
||||
test(`${distribution}: empty legacy sources.list falls back to deb822 .sources`, async (t) => {
|
||||
const root = await fixture(t);
|
||||
const legacy = path.join(root, 'sources.list');
|
||||
const sources = path.join(
|
||||
root,
|
||||
'sources.list.d',
|
||||
`${distribution.toLowerCase()}.sources`,
|
||||
);
|
||||
await fs.writeFile(legacy, legacyContent);
|
||||
const util = load('back/config/util.ts', {
|
||||
'fs/promises': {
|
||||
readFile: async (file) => {
|
||||
if (file === '/etc/os-release')
|
||||
return `ID=${distribution.toLowerCase()}\nVERSION_CODENAME=${
|
||||
distribution === 'Debian' ? 'bookworm' : 'noble'
|
||||
}\n`;
|
||||
if (file === '/etc/apt/sources.list')
|
||||
return fs.readFile(legacy, 'utf8');
|
||||
throw Object.assign(new Error('missing fixture source'), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
},
|
||||
},
|
||||
os: { platform: () => 'linux' },
|
||||
'./index': {},
|
||||
'./const': {},
|
||||
'./share': {},
|
||||
'./container': { isInContainer: () => false },
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../shared/fileAccess': {},
|
||||
'../data/dependence': {},
|
||||
});
|
||||
const previousOS = process.env.QL_OS_TYPE;
|
||||
let command;
|
||||
try {
|
||||
process.env.QL_OS_TYPE = distribution.toLowerCase();
|
||||
command = await util.updateLinuxMirrorFile('https://mirror.example');
|
||||
} finally {
|
||||
if (previousOS === undefined) delete process.env.QL_OS_TYPE;
|
||||
else process.env.QL_OS_TYPE = previousOS;
|
||||
}
|
||||
// Execute the entire generated writer, redirecting its APT paths into
|
||||
// the fixture and replacing only the package-index refresh.
|
||||
const isolated = command
|
||||
.replaceAll('/etc/apt', root)
|
||||
.replace('apt-get update --error-on=any', 'true');
|
||||
const result = spawnSync('bash', ['-c', isolated], { encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(await fs.readFile(legacy, 'utf8'), legacyContent);
|
||||
const generated = await fs.readFile(sources, 'utf8');
|
||||
assert.match(generated, /^Types: deb\nURIs: https:\/\/mirror\.example\//);
|
||||
assert.match(generated, /Signed-By: \/usr\/share\/keyrings\//);
|
||||
assert.match(
|
||||
generated,
|
||||
distribution === 'Debian' ? /bookworm-security/ : /noble-backports/,
|
||||
);
|
||||
if (hasApt) {
|
||||
// Parse both source locations with real APT, without refreshing indexes
|
||||
// or touching the host's configuration/cache.
|
||||
const parsed = spawnSync(
|
||||
'apt-get',
|
||||
[
|
||||
'indextargets',
|
||||
'-o',
|
||||
`Dir::Etc::sourcelist=${legacy}`,
|
||||
'-o',
|
||||
`Dir::Etc::sourceparts=${path.dirname(sources)}`,
|
||||
'-o',
|
||||
`Dir::State::lists=${path.join(root, 'lists')}`,
|
||||
],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(parsed.status, 0, parsed.stderr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('deb822 mirror changes preserve vendor archives, signing keys and comments across repeated saves', () => {
|
||||
const system =
|
||||
'Types: deb\nURIs: https://deb.debian.org/debian\nSuites: bookworm\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n';
|
||||
const vendors = [
|
||||
'Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: bookworm\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\n',
|
||||
'Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: bookworm\nComponents: stable\n',
|
||||
'Types: deb\nURIs: https://packages.microsoft.com/repos/code\nSuites: stable\nComponents: main\n',
|
||||
'Types: deb\nURIs: https://vendor.example/debian\nSuites: bookworm\nComponents: main\nSigned-By: /etc/apt/keyrings/vendor.gpg\n',
|
||||
];
|
||||
const comment = '# Keep https://vendor.example/debian unchanged\n';
|
||||
const original = [system, ...vendors, comment].join('\n');
|
||||
const first = rewriteMirrorSources(
|
||||
original,
|
||||
'https://mirror.example/mirrors/debian',
|
||||
);
|
||||
assert.ok(first.includes('URIs: https://mirror.example/mirrors/debian\n'));
|
||||
for (const unchanged of [...vendors, comment])
|
||||
assert.ok(first.includes(unchanged));
|
||||
assert.equal(
|
||||
rewriteMirrorSources(first, 'https://mirror.example/mirrors/debian'),
|
||||
first,
|
||||
);
|
||||
const second = rewriteMirrorSources(first, 'https://second.example/debian');
|
||||
assert.ok(second.includes('URIs: https://second.example/debian\n'));
|
||||
for (const unchanged of [...vendors, comment])
|
||||
assert.ok(second.includes(unchanged));
|
||||
});
|
||||
|
||||
test('legacy mirror changes preserve third-party lines, inline comments and source options', () => {
|
||||
const system =
|
||||
'deb [arch=amd64 signed-by=/usr/share/keyrings/debian-archive-keyring.gpg] https://deb.debian.org/debian bookworm main # https://leave.example/debian\n';
|
||||
const security =
|
||||
'deb-src https://security.debian.org/debian-security bookworm-security main\n';
|
||||
const vendors = [
|
||||
'deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\n',
|
||||
'deb https://download.docker.com/linux/debian bookworm stable\n',
|
||||
'deb [signed-by=/etc/apt/keyrings/vendor.gpg] https://vendor.example/debian bookworm main\n',
|
||||
'deb https://packages.microsoft.com/repos/code stable main\n',
|
||||
'# deb https://deb.debian.org/debian bookworm main\n',
|
||||
];
|
||||
const original = system + security + vendors.join('');
|
||||
const changed = rewriteMirrorSources(
|
||||
original,
|
||||
'https://mirror.example/debian',
|
||||
);
|
||||
assert.ok(
|
||||
changed.includes(
|
||||
system.replace(
|
||||
'https://deb.debian.org/debian',
|
||||
'https://mirror.example/debian',
|
||||
),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
changed.includes(
|
||||
security.replace(
|
||||
'https://security.debian.org/debian-security',
|
||||
'https://mirror.example/debian-security',
|
||||
),
|
||||
),
|
||||
);
|
||||
for (const unchanged of vendors) assert.ok(changed.includes(unchanged));
|
||||
assert.equal(
|
||||
rewriteMirrorSources(changed, 'https://mirror.example/debian'),
|
||||
changed,
|
||||
);
|
||||
});
|
||||
|
||||
test('Ubuntu ports, Armbian and Alpine roots still switch mirrors without altering repository metadata', () => {
|
||||
for (const url of [
|
||||
'https://ports.ubuntu.com/ubuntu-ports',
|
||||
'https://apt.armbian.com/armbian',
|
||||
'https://old.example/alpine/v3.22/main',
|
||||
'https://old.example/alpine/v3.22/community',
|
||||
]) {
|
||||
const original = `URIs: ${url}\nSuites: noble\nComponents: main\n`;
|
||||
assert.equal(
|
||||
rewriteMirrorSources(original, 'https://mirror.example'),
|
||||
original.replace(url, replaceMirrorUrl(url, 'https://mirror.example')),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -129,7 +129,7 @@ Acquire::http::Timeout "1";
|
||||
'fs/promises': {
|
||||
readFile: async (file) =>
|
||||
file === '/etc/os-release'
|
||||
? `${distribution}\nVERSION_CODENAME=bookworm\n`
|
||||
? distribution
|
||||
: existingMirror
|
||||
? 'URIs: http://old.example/debian\n'
|
||||
: '',
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const test = require('node:test'),
|
||||
assert = require('node:assert/strict'),
|
||||
fs = require('node:fs'),
|
||||
os = require('node:os'),
|
||||
path = require('node:path'),
|
||||
{ execFileSync } = require('node:child_process');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
test('Python source arguments round-trip literally and local files stay inside maintained roots', (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-pip-source-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const scripts = path.join(root, 'scripts'),
|
||||
repo = path.join(root, 'repo');
|
||||
fs.mkdirSync(scripts);
|
||||
fs.mkdirSync(repo);
|
||||
fs.mkdirSync(path.join(scripts, 'project space'));
|
||||
const requirement = path.join(scripts, 'project space', 'requirements.txt');
|
||||
fs.writeFileSync(requirement, 'requests');
|
||||
fs.writeFileSync(
|
||||
path.join(scripts, 'project space', 'pyproject.toml'),
|
||||
'[project]\nname="sample"',
|
||||
);
|
||||
const outside = path.join(root, 'requirements.txt');
|
||||
fs.writeFileSync(outside, 'outside');
|
||||
fs.symlinkSync(outside, path.join(scripts, 'requirements-link.txt'));
|
||||
const api = load('back/config/util.ts', {
|
||||
'./index': { scriptPath: scripts, repoPath: repo },
|
||||
'./const': { PYTHON_INSTALL_DIR: '/prefix with spaces' },
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../data/dependence': {
|
||||
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||
},
|
||||
});
|
||||
function args(cmd) {
|
||||
return execFileSync('/bin/bash', [
|
||||
'-c',
|
||||
'pip3(){ printf "%s\\0" "$@"; }; ' + cmd,
|
||||
])
|
||||
.toString()
|
||||
.split('\0')
|
||||
.slice(0, -1);
|
||||
}
|
||||
const req = args(api.getInstallCommand(1, 'project space/requirements.txt'));
|
||||
assert.equal(req.at(-1), requirement);
|
||||
assert.equal(req.at(-2), '-r');
|
||||
assert.ok(req.includes('--prefix=/prefix with spaces'));
|
||||
assert.equal(
|
||||
args(api.getInstallCommand(1, 'project space/pyproject.toml')).at(-1),
|
||||
path.dirname(requirement),
|
||||
);
|
||||
const url = 'git+https://example.invalid/repo.git#egg=pkg;$(echo INJECTED)';
|
||||
assert.equal(args(api.getInstallCommand(1, url)).at(-1), url);
|
||||
assert.throws(() => api.getInstallCommand(1, '../requirements.txt'));
|
||||
assert.throws(() => api.getInstallCommand(1, 'requirements-link.txt'));
|
||||
assert.equal(
|
||||
args(api.getUninstallCommand(1, 'project space/requirements.txt')).at(-1),
|
||||
requirement,
|
||||
);
|
||||
assert.match(
|
||||
api.getUninstallCommand(1, url),
|
||||
/Remove installed Python distributions separately/,
|
||||
);
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
|
||||
test('requirements install, probe and removal stay in the selected venv', (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-source-venv-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const venv = path.join(root, 'virtual env');
|
||||
const scripts = path.join(root, 'scripts');
|
||||
fs.mkdirSync(scripts);
|
||||
execFileSync('python3', ['-m', 'venv', venv]);
|
||||
const wheel = path.join(scripts, 'ql_venv_fixture-1.0-py3-none-any.whl');
|
||||
execFileSync('python3', [
|
||||
'-c',
|
||||
`import zipfile,sys
|
||||
with zipfile.ZipFile(sys.argv[1],'w') as z:
|
||||
z.writestr('ql_venv_fixture.py','value = 42')
|
||||
z.writestr('ql_venv_fixture-1.0.dist-info/METADATA','Metadata-Version: 2.1\\nName: ql-venv-fixture\\nVersion: 1.0\\n')
|
||||
z.writestr('ql_venv_fixture-1.0.dist-info/WHEEL','Wheel-Version: 1.0\\nGenerator: fixture\\nRoot-Is-Purelib: true\\nTag: py3-none-any\\n')
|
||||
z.writestr('ql_venv_fixture-1.0.dist-info/RECORD','')`,
|
||||
wheel,
|
||||
]);
|
||||
fs.writeFileSync(path.join(scripts, 'requirements.txt'), wheel + '\n');
|
||||
const api = load('back/config/util.ts', {
|
||||
'./index': {
|
||||
rootPath: root,
|
||||
scriptPath: scripts,
|
||||
repoPath: path.join(root, 'repo'),
|
||||
},
|
||||
'./const': {
|
||||
PYTHON_VENV_DIR: 'virtual env',
|
||||
PYTHON_INSTALL_DIR: '/unused-global-prefix',
|
||||
},
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../data/dependence': {
|
||||
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||
},
|
||||
});
|
||||
const run = (command) =>
|
||||
execFileSync('/bin/bash', ['-c', command], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
PIP_NO_INDEX: '1',
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: '1',
|
||||
},
|
||||
});
|
||||
const install = api.getInstallCommand(1, 'requirements.txt');
|
||||
assert.doesNotMatch(install, /--prefix=/);
|
||||
run(install);
|
||||
assert.match(run(api.getGetCommand(1, 'ql-venv-fixture')), /1\.0/);
|
||||
assert.equal(
|
||||
run(
|
||||
`'${path.join(
|
||||
venv,
|
||||
'bin/python3',
|
||||
)}' -c 'import ql_venv_fixture; print(ql_venv_fixture.value)'`,
|
||||
).trim(),
|
||||
'42',
|
||||
);
|
||||
run(api.getUninstallCommand(1, 'requirements.txt'));
|
||||
assert.throws(() =>
|
||||
run(`'${path.join(venv, 'bin/python3')}' -c 'import ql_venv_fixture'`),
|
||||
);
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
const test = require('node:test'),
|
||||
assert = require('node:assert/strict'),
|
||||
fs = require('node:fs'),
|
||||
os = require('node:os'),
|
||||
path = require('node:path'),
|
||||
{ execFileSync } = require('node:child_process');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
test('venv installs, uninstalls and probes use the selected Python, including spaces', () => {
|
||||
const methods = load('back/config/util.ts', {
|
||||
'./index': { rootPath: '/application' },
|
||||
'./const': {
|
||||
PYTHON_VENV_DIR: 'virtual env',
|
||||
PYTHON_INSTALL_DIR: '/legacy',
|
||||
},
|
||||
'../loaders/logger': {},
|
||||
'../shared/utils': {},
|
||||
'../data/dependence': {
|
||||
DependenceTypes: { nodejs: 0, python3: 1, linux: 2 },
|
||||
},
|
||||
});
|
||||
for (const name of [
|
||||
'getInstallCommand',
|
||||
'getUninstallCommand',
|
||||
'getGetCommand',
|
||||
]) {
|
||||
const cmd = methods[name](1, 'requests');
|
||||
assert.match(cmd, /'\/application\/virtual env\/bin\/python3'/);
|
||||
assert.doesNotMatch(cmd, /--prefix=/);
|
||||
}
|
||||
});
|
||||
test('venv setup keeps Python packages isolated and retains existing startup configuration', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-venv-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
const ql = path.join(dir, 'application with spaces');
|
||||
fs.mkdirSync(ql);
|
||||
const venv = path.join(dir, 'venv with spaces');
|
||||
execFileSync('python3', ['-m', 'venv', venv]);
|
||||
const source = fs.readFileSync('shell/start-simplify.sh', 'utf8');
|
||||
const snippet = source.slice(
|
||||
source.indexOf('export PNPM_HOME='),
|
||||
source.indexOf('\ncd "${QL_DIR}"'),
|
||||
);
|
||||
const result = execFileSync(
|
||||
'/bin/bash',
|
||||
[
|
||||
'-c',
|
||||
snippet +
|
||||
'\npython3 -c "import sys; print(sys.prefix); print(sys.base_prefix)"',
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
QL_DIR: ql,
|
||||
QL_DATA_DIR: path.join(dir, 'data'),
|
||||
PYTHON_VENV_DIR: venv,
|
||||
PYTHONPATH: '/legacy/modules',
|
||||
PYTHONUSERBASE: '/legacy',
|
||||
PATH: process.env.PATH,
|
||||
},
|
||||
encoding: 'utf8',
|
||||
},
|
||||
)
|
||||
.trim()
|
||||
.split('\n');
|
||||
assert.equal(fs.realpathSync(result[0]), fs.realpathSync(venv));
|
||||
assert.notEqual(result[1], venv);
|
||||
assert.match(source, /set -C; cat .env.example > .env/);
|
||||
assert.doesNotMatch(source, /npm install|pip3 install|chmod 777/);
|
||||
});
|
||||
@@ -7,8 +7,8 @@ const { spawnSync } = require('node:child_process');
|
||||
|
||||
// Run the actual startup prefix, stopping before service initialization.
|
||||
const source = fs
|
||||
.readFileSync('shell/start-simplify.sh', 'utf8')
|
||||
.split('. "${QL_DIR}/shell/share.sh"')[0];
|
||||
.readFileSync('shell/start.sh', 'utf8')
|
||||
.split('. ${QL_DIR}/shell/share.sh')[0];
|
||||
for (const command of ['start', 'reload']) {
|
||||
for (const existing of [false, true]) {
|
||||
test(`${command} ${
|
||||
|
||||
Reference in New Issue
Block a user