fix(python): keep source dependency installs and removals in selected venv

This commit is contained in:
whyour
2026-10-13 02:21:10 +08:00
14 changed files with 984 additions and 126 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ jobs:
run: | run: |
docker run --rm --network none --entrypoint node \ docker run --rm --network none --entrypoint node \
qinglong-readiness:${{ matrix.image }} \ 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.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")' -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")'
- name: Export native Debian image for isolated dev validation - name: Export native Debian image for isolated dev validation
if: matrix.image == 'debian' if: matrix.image == 'debian'
run: | run: |
+1
View File
@@ -27,6 +27,7 @@ export const SAMPLE_FILES = [
]; ];
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME; export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
export const PYTHON_VENV_DIR = process.env.PYTHON_VENV_DIR;
export const NotificationModeStringMap = { export const NotificationModeStringMap = {
0: 'gotify', 0: 'gotify',
+88 -92
View File
@@ -6,7 +6,7 @@ import psTreeFun from 'ps-tree';
import { promisify } from 'util'; import { promisify } from 'util';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import config from './index'; import config from './index';
import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const'; import { PYTHON_INSTALL_DIR, PYTHON_VENV_DIR, TASK_COMMAND } from './const';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils'; import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence'; import { DependenceTypes } from '../data/dependence';
@@ -15,6 +15,11 @@ import os from 'os';
import { maybeSudo, isInContainer } from './container'; import { maybeSudo, isInContainer } from './container';
import { shellQuote } from '../shared/shellQuote'; import { shellQuote } from '../shared/shellQuote';
import { resolveFileAccess } from '../shared/fileAccess'; import { resolveFileAccess } from '../shared/fileAccess';
import {
mirrorWriteCommand,
normalizeMirror,
rewriteMirrorSources,
} from '../shared/linuxMirror';
export * from './share'; export * from './share';
@@ -685,6 +690,16 @@ export async function setSystemTimezone(timezone: string): Promise<boolean> {
} }
// Helper function to check if a name is a GitHub URL // 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 { function isGitHubUrl(name: string): boolean {
// Support git+https://, git+http://, https://, and http:// URLs // Support git+https://, git+http://, https://, and http:// URLs
// This covers GitHub URLs and other git-compatible repositories // This covers GitHub URLs and other git-compatible repositories
@@ -720,7 +735,7 @@ export function getGetCommand(type: DependenceTypes, name: string): string {
const baseCommands = { const baseCommands = {
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`, [DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`,
[DependenceTypes.python3]: `python3 -c ${shellQuote( [DependenceTypes.python3]: `${getPythonExecutable()} -c ${shellQuote(
`from importlib.metadata import version\nimport importlib.util as u\nname=${JSON.stringify( `from importlib.metadata import version\nimport importlib.util as u\nname=${JSON.stringify(
trimmedName, trimmedName,
)}\ntry:\n print(version(name))\nexcept Exception:\n print(name if u.find_spec(name) else '')`, )}\ntry:\n print(version(name))\nexcept Exception:\n print(name if u.find_spec(name) else '')`,
@@ -736,10 +751,13 @@ export function getGetCommand(type: DependenceTypes, name: string): string {
export function getInstallCommand(type: DependenceTypes, name: string): string { export function getInstallCommand(type: DependenceTypes, name: string): string {
if (type === DependenceTypes.python3) { if (type === DependenceTypes.python3) {
const prefix = PYTHON_INSTALL_DIR const prefix =
PYTHON_INSTALL_DIR && !PYTHON_VENV_DIR
? ` --prefix=${shellQuote(PYTHON_INSTALL_DIR)}` ? ` --prefix=${shellQuote(PYTHON_INSTALL_DIR)}`
: ''; : '';
const pip = `pip3 install --disable-pip-version-check --root-user-action=ignore${prefix}`; const pip = `${getPythonPip()} install --disable-pip-version-check${
PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
}${prefix}`;
name = name.trim(); name = name.trim();
if (isRequirementsFile(name) || isPyprojectToml(name)) { if (isRequirementsFile(name) || isPyprojectToml(name)) {
const file = [config.scriptPath, config.repoPath] const file = [config.scriptPath, config.repoPath]
@@ -795,9 +813,9 @@ export function getUninstallCommand(
throw new Error( throw new Error(
'Python dependency file must be inside scripts or repo', 'Python dependency file must be inside scripts or repo',
); );
return `pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y -r ${shellQuote( return `${getPythonPip()} uninstall --disable-pip-version-check${
file, PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
)}`; } -y -r ${shellQuote(file)}`;
} }
if (isPyprojectToml(name) || isGitHubUrl(name)) { if (isPyprojectToml(name) || isGitHubUrl(name)) {
// A source URL/project can install several distributions and has no safe // A source URL/project can install several distributions and has no safe
@@ -806,9 +824,9 @@ export function getUninstallCommand(
'Remove installed Python distributions separately by package name.', 'Remove installed Python distributions separately by package name.',
)}`; )}`;
} }
return `pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y -- ${shellQuote( return `${getPythonPip()} uninstall --disable-pip-version-check${
name, PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore'
)}`; } -y -- ${shellQuote(name)}`;
} }
const baseCommands = { const baseCommands = {
[DependenceTypes.nodejs]: 'pnpm remove -g', [DependenceTypes.nodejs]: 'pnpm remove -g',
@@ -886,95 +904,73 @@ export async function detectOS(): Promise<
return osType; 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( async function _updateLinuxMirror(
osType: string, osType: string,
mirrorDomainWithScheme: string, mirror: string,
): Promise<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 S = isInContainer() ? 'sudo ' : '';
// APT otherwise returns zero when index downloads fail with transient errors. const update =
const aptUpdate = `${S}apt-get update --error-on=any`; osType === 'Alpine' ? 'apk update' : `${S}apt-get update --error-on=any`;
let filePath: string, currentDomainWithScheme: string | null; let file =
switch (osType) { osType === 'Alpine'
case 'Debian': ? '/etc/apk/repositories'
filePath = '/etc/apt/sources.list.d/debian.sources'; : `/etc/apt/sources.list.d/${osType.toLowerCase()}.sources`;
currentDomainWithScheme = await getCurrentMirrorDomain(filePath); let content = '';
if (currentDomainWithScheme) { try {
return `${S}sed -i 's|${currentDomainWithScheme}|${ content = await fs.readFile(file, 'utf8');
mirrorDomainWithScheme || 'http://deb.debian.org' } catch (error: any) {
}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${ if (error.code !== 'ENOENT') throw error;
mirrorDomainWithScheme || 'http://deb.debian.org' if (osType !== 'Alpine') {
}\\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}`; 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`;
}
}
}
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 { } else {
return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${ // The fallback may have read an empty legacy .list. Deb822 stanzas must
mirrorDomainWithScheme || 'http://deb.debian.org' // be written to a .sources file, leaving the legacy file untouched.
}\\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}`; 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`;
} }
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.');
} }
return mirrorWriteCommand(
file,
rewriteMirrorSources(content, target),
S,
update,
);
} }
export async function updateLinuxMirrorFile(mirror: string): Promise<string> { export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
+8 -1
View File
@@ -179,7 +179,14 @@ export default ({ app }: { app: Application }) => {
app.use(rewrite('/open/*', '/api/$1')); app.use(rewrite('/open/*', '/api/$1'));
app.use(config.api.prefix, routes()); app.use(config.api.prefix, routes());
app.get('*', (_, res, next) => { 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' });
}
const indexPath = path.join(frontendPath, 'index.html'); const indexPath = path.join(frontendPath, 'index.html');
res.sendFile(indexPath, (err) => { res.sendFile(indexPath, (err) => {
if (err) { if (err) {
+121
View File
@@ -0,0 +1,121 @@
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}`;
}
@@ -409,7 +409,7 @@ test('cron metadata precedence agrees with unchanged legacy add_cron', (t) => {
'--noprofile', '--noprofile',
'--norc', '--norc',
'-c', '-c',
`${definition}\nt() { :; }\nnotify_api() { :; }\nadd_cron_api() { printf '%s' "$1"; }\nadd_cron "$dir_scripts/list" repo`, `${definition}\nt() { :; }\nnotify_api() { :; }\nadd_cron_api() { printf '%s:%s:%s:%s' "$1" "$2" "$3" "$4"; }\nadd_cron "$dir_scripts/list" repo`,
], ],
{ {
encoding: 'utf8', encoding: 'utf8',
+2 -2
View File
@@ -108,9 +108,9 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"keyv": "^5.2.3", "keyv": "^5.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "2.3.0", "multer": "2.4.0",
"node-cron": "4.6.0", "node-cron": "4.6.0",
"nodemailer": "^8.0.1", "nodemailer": "^10.0.9",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
+354 -20
View File
@@ -28,7 +28,7 @@ dependencies:
version: 12.0.1 version: 12.0.1
body-parser: body-parser:
specifier: ^1.20.3 specifier: ^1.20.3
version: 1.20.3 version: 1.20.6
celebrate: celebrate:
specifier: ^15.0.3 specifier: ^15.0.3
version: 15.0.3 version: 15.0.3
@@ -73,7 +73,7 @@ dependencies:
version: 1.2.0 version: 1.2.0
http-proxy-middleware: http-proxy-middleware:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.7
iconv-lite: iconv-lite:
specifier: ^0.6.3 specifier: ^0.6.3
version: 0.6.3 version: 0.6.3
@@ -82,7 +82,7 @@ dependencies:
version: 2.3.0(@types/node@17.0.45) version: 2.3.0(@types/node@17.0.45)
js-yaml: js-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.3.2
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
@@ -93,14 +93,14 @@ dependencies:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
multer: multer:
specifier: 2.3.0 specifier: 2.4.0
version: 2.3.0 version: 2.4.0
node-cron: node-cron:
specifier: 4.6.0 specifier: 4.6.0
version: 4.6.0 version: 4.6.0
nodemailer: nodemailer:
specifier: ^8.0.1 specifier: ^10.0.9
version: 8.0.1 version: 10.0.9
p-queue-cjs: p-queue-cjs:
specifier: 7.3.4 specifier: 7.3.4
version: 7.3.4 version: 7.3.4
@@ -133,7 +133,7 @@ dependencies:
version: 0.10.0 version: 0.10.0
undici: undici:
specifier: ^7.9.0 specifier: ^7.9.0
version: 7.9.0 version: 7.29.1
uuid: uuid:
specifier: ^11.0.3 specifier: ^11.0.3
version: 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) version: 4.23.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
axios: axios:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.7.8 version: 1.20.0
compression-webpack-plugin: compression-webpack-plugin:
specifier: 9.2.0 specifier: 9.2.0
version: 9.2.0 version: 9.2.0
@@ -2879,7 +2879,7 @@ packages:
globals: 13.24.0 globals: 13.24.0
ignore: 5.3.2 ignore: 5.3.2
import-fresh: 3.3.0 import-fresh: 3.3.0
js-yaml: 4.1.0 js-yaml: 4.3.2
minimatch: 3.1.5 minimatch: 3.1.5
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -3060,7 +3060,7 @@ packages:
camelcase: 5.3.1 camelcase: 5.3.1
find-up: 4.1.0 find-up: 4.1.0
get-package-type: 0.1.0 get-package-type: 0.1.0
js-yaml: 3.14.1 js-yaml: 3.15.2
resolve-from: 5.0.0 resolve-from: 5.0.0
dev: true dev: true
@@ -5677,7 +5677,6 @@ packages:
debug: 4.4.3 debug: 4.4.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false
/agentkeepalive@4.5.0: /agentkeepalive@4.5.0:
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
@@ -8302,7 +8301,7 @@ packages:
is-glob: 4.0.3 is-glob: 4.0.3
is-path-inside: 3.0.3 is-path-inside: 3.0.3
js-sdsl: 4.4.2 js-sdsl: 4.4.2
js-yaml: 4.1.0 js-yaml: 4.3.2
json-stable-stringify-without-jsonify: 1.0.1 json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1 levn: 0.4.1
lodash.merge: 4.6.2 lodash.merge: 4.6.2
@@ -8513,7 +8512,7 @@ packages:
dependencies: dependencies:
accepts: 1.3.8 accepts: 1.3.8
array-flatten: 1.1.1 array-flatten: 1.1.1
body-parser: 1.20.3 body-parser: 1.20.6
content-disposition: 0.5.4 content-disposition: 0.5.4
content-type: 1.0.5 content-type: 1.0.5
cookie: 0.7.1 cookie: 0.7.1
@@ -9334,7 +9333,6 @@ packages:
debug: 4.4.3 debug: 4.4.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false
/human-signals@2.1.0: /human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
@@ -10587,7 +10585,7 @@ packages:
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
dependencies: dependencies:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.2
/miller-rabin@4.0.1: /miller-rabin@4.0.1:
resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==}
@@ -10654,19 +10652,19 @@ packages:
/minimatch@3.1.2: /minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.21
dev: true dev: true
/minimatch@3.1.5: /minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.21
/minimatch@9.0.9: /minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
dependencies: dependencies:
brace-expansion: 2.0.2 brace-expansion: 2.1.7
dev: true dev: true
/minimist-options@4.1.0: /minimist-options@4.1.0:
@@ -10870,7 +10868,7 @@ packages:
semver: 7.7.4 semver: 7.7.4
tar: 7.5.20 tar: 7.5.20
tinyglobby: 0.2.17 tinyglobby: 0.2.17
undici: 6.27.0 undici: 6.29.0
which: 6.0.1 which: 6.0.1
dev: false dev: false
optional: true optional: true
@@ -15794,6 +15792,342 @@ packages:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
dev: true 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: settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
+14 -3
View File
@@ -85,9 +85,13 @@ add_cron_api() {
sub_id="null" sub_id="null"
fi 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 \ local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \ "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \ --data-raw "$payload" \
-X POST) -X POST)
code=$(echo "$api" | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message) message=$(echo "$api" | jq -r .message)
@@ -112,10 +116,14 @@ update_cron_api() {
local id="$4" local id="$4"
fi 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 \ local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \ "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X PUT \ -X PUT \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}") --data-raw "$payload")
code=$(echo "$api" | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
@@ -135,10 +143,13 @@ update_cron_command_api() {
local id="$2" local id="$2"
fi fi
local payload
payload=$(jq -cn --arg command "$command" --arg id "$id" \
'{command: $command, id: $id}') || return $?
local api=$(ql_api_request \ local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \ "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X PUT \ -X PUT \
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}") --data-raw "$payload")
code=$(echo "$api" | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
+6 -2
View File
@@ -99,7 +99,9 @@ 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) 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=$(grep "name:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_name ]] && cron_name=$(basename "$file_name") [[ -z $cron_name ]] && cron_name=$(basename "$file_name")
result=$(add_cron_api "${cron_line}:${cmd_task} ${file}:${cron_name}:${SUB_ID}") cron_name=${cron_name//$'\r'/}
cron_line=${cron_line//$'\r'/}
result=$(add_cron_api "$cron_line" "${cmd_task} ${file}" "$cron_name" "$SUB_ID")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
@@ -190,7 +192,9 @@ 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 ":" '{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=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "${cron_line}:${cmd_task} ${filename}:${cron_name}:${SUB_ID}") cron_name=${cron_name//$'\r'/}
cron_line=${cron_line//$'\r'/}
result=$(add_cron_api "$cron_line" "${cmd_task} ${filename}" "$cron_name" "$SUB_ID")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
+61
View File
@@ -0,0 +1,61 @@
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');
});
+253
View File
@@ -0,0 +1,253 @@
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')),
);
}
});
+1 -1
View File
@@ -129,7 +129,7 @@ Acquire::http::Timeout "1";
'fs/promises': { 'fs/promises': {
readFile: async (file) => readFile: async (file) =>
file === '/etc/os-release' file === '/etc/os-release'
? distribution ? `${distribution}\nVERSION_CODENAME=bookworm\n`
: existingMirror : existingMirror
? 'URIs: http://old.example/debian\n' ? 'URIs: http://old.example/debian\n'
: '', : '',
+70
View File
@@ -0,0 +1,70 @@
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'`),
);
});