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 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'; let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined; function getOsTypeSync(): 'Debian' | 'Ubuntu' | 'Alpine' | undefined { // 1. 环境变量覆盖 const envOs = process.env.QL_OS_TYPE?.toLowerCase(); if (envOs === 'alpine') return 'Alpine'; if (envOs === 'debian') return 'Debian'; if (envOs === 'ubuntu') return 'Ubuntu'; // 2. 模块缓存(由 detectOS 设置) if (osType) return osType; // 3. 能力检测:检查包管理器二进制 try { execSync('which apt-get', { stdio: 'ignore' }); return 'Debian'; } catch { try { execSync('which apk', { stdio: 'ignore' }); return 'Alpine'; } catch { // macOS / 未知系统 } } return undefined; } export async function getFileContentByName(fileName: string) { const _exsit = await fileExist(fileName); if (_exsit) { return await fs.readFile(fileName, 'utf8'); } return ''; } export function removeAnsi(text: string) { return text.replace(/\x1b\[\d+m/g, ''); } export async function getLastModifyFilePath(dir: string) { let filePath = ''; const _exsit = await fileExist(dir); if (_exsit) { const arr = await fs.readdir(dir); arr.forEach(async (item) => { const fullpath = path.join(dir, item); const stats = await fs.lstat(fullpath); if (stats.isFile()) { if (stats.mtimeMs >= 0) { filePath = fullpath; } } }); } return filePath; } export function getToken(req: any) { const { authorization = '' } = req.headers; if (authorization && authorization.split(' ')[0] === 'Bearer') { return (authorization as string) .replace('Bearer ', '') .replace('mobile-', '') .replace('desktop-', ''); } return ''; } export function getPlatform(userAgent: string): 'mobile' | 'desktop' { const ua = userAgent.toLowerCase(); const testUa = (regexp: RegExp) => regexp.test(ua); const testVs = (regexp: RegExp) => (ua.match(regexp) || []) .toString() .replace(/[^0-9|_.]/g, '') .replace(/_/g, '.'); // 系统 let system = 'unknow'; if (testUa(/windows|win32|win64|wow32|wow64/g)) { system = 'windows'; // windows系统 } else if (testUa(/macintosh|macintel/g)) { system = 'macos'; // macos系统 } else if (testUa(/x11/g)) { system = 'linux'; // linux系统 } else if (testUa(/android|adr/g)) { system = 'android'; // android系统 } else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) { system = 'ios'; // ios系统 } else if (testUa(/openharmony/g)) { system = 'openharmony'; // openharmony系统 } let platform = 'desktop'; if (system === 'windows' || system === 'macos' || system === 'linux') { platform = 'desktop'; } else if ( system === 'android' || system === 'ios' || system === 'openharmony' || testUa(/mobile/g) ) { platform = 'mobile'; } return platform as 'mobile' | 'desktop'; } export async function fileExist(file: any) { try { await fs.access(file); return true; } catch (error) { return false; } } export async function createFile(file: string, data: string = '') { await fs.mkdir(path.dirname(file), { recursive: true }); await writeFileWithLock(file, data); } export async function handleLogPath( logPath: string, data: string = '', ): Promise { const absolutePath = resolveFileAccess(config.logPath, [logPath]); if (!absolutePath) throw new Error('Log path is outside the log directory'); const logFileExist = await fileExist(absolutePath); if (!logFileExist) { await createFile(absolutePath, data); } return absolutePath; } export async function concurrentRun( fnList: Array<() => Promise> = [], max = 5, ) { if (!fnList.length) return; const replyList: any[] = []; // 收集任务执行结果 const startTime = new Date().getTime(); // 记录任务执行开始时间 // 任务执行程序 const schedule = async (index: number) => { return new Promise(async (resolve) => { const fn = fnList[index]; if (!fn) return resolve(null); // 执行当前异步任务 const reply = await fn(); replyList[index] = reply; // 执行完当前任务后,继续执行任务池的剩余任务 await schedule(index + max); resolve(null); }); }; // 任务池执行程序 const scheduleList = new Array(max) .fill(0) .map((_, index) => schedule(index)); // 使用 Promise.all 批量执行 const r = await Promise.all(scheduleList); const cost = (new Date().getTime() - startTime) / 1000; return replyList; } enum FileType { 'directory', 'file', } export interface IFile { title: string; key: string; type: 'directory' | 'file'; parent: string; createTime: number; size?: number; children?: IFile[]; } export function dirSort(a: IFile, b: IFile): number { if (a.type === 'file' && b.type === 'file') { return b.createTime - a.createTime; } else if (a.type === 'directory' && b.type === 'directory') { return a.title.localeCompare(b.title); } else { return a.type === 'directory' ? -1 : 1; } } const FILE_SYSTEM_READ_CONCURRENCY = 32; type FileSystemTaskRunner = (task: () => Promise) => Promise; function createFileSystemTaskRunner(concurrency: number): FileSystemTaskRunner { let activeCount = 0; const queue: Array<() => void> = []; const runNext = () => { while (activeCount < concurrency && queue.length > 0) { activeCount += 1; queue.shift()?.(); } }; return (task: () => Promise) => new Promise((resolve, reject) => { queue.push(() => { task() .then(resolve, reject) .finally(() => { activeCount -= 1; runNext(); }); }); runNext(); }); } async function readDirsWithRunner( dir: string, baseDir: string, blacklist: string[], sort: (a: IFile, b: IFile) => number, runFileSystemTask: FileSystemTaskRunner, ): Promise { const relativePath = path.relative(baseDir, dir); const entries = await runFileSystemTask(() => fs.readdir(dir, { withFileTypes: true }), ); const items = await Promise.all( entries.map(async (entry): Promise => { if (blacklist.includes(entry.name) || entry.isSymbolicLink()) { return undefined; } const subPath = path.join(dir, entry.name); const stats = await runFileSystemTask(() => fs.lstat(subPath)); if (stats.isSymbolicLink()) { return undefined; } const key = path.join(relativePath, entry.name); if (stats.isDirectory()) { const children = await readDirsWithRunner( subPath, baseDir, blacklist, sort, runFileSystemTask, ); return { title: entry.name, key, type: 'directory', parent: relativePath, createTime: stats.birthtime.getTime(), children, }; } return { title: entry.name, type: 'file', key, parent: relativePath, size: stats.size, createTime: stats.birthtime.getTime(), }; }), ); return items.filter((item): item is IFile => Boolean(item)).sort(sort); } export async function readDirs( dir: string, baseDir: string = '', blacklist: string[] = [], sort: (a: IFile, b: IFile) => number = dirSort, ): Promise { return readDirsWithRunner( dir, baseDir, blacklist, sort, createFileSystemTaskRunner(FILE_SYSTEM_READ_CONCURRENCY), ); } export async function readDir( dir: string, baseDir: string = '', blacklist: string[] = [], ): Promise { const absoluteDir = path.resolve(baseDir, dir); if (!absoluteDir.startsWith(path.resolve(baseDir))) { return []; } const relativePath = path.relative(baseDir, absoluteDir); try { const files = await fs.readdir(absoluteDir); const result: IFile[] = []; for (const file of files) { const subPath = path.join(absoluteDir, file); const stats = await fs.lstat(subPath); const key = path.join(relativePath, file); if (blacklist.includes(file) || stats.isSymbolicLink()) { continue; } if (stats.isDirectory()) { result.push({ title: file, type: 'directory', key, parent: relativePath, createTime: stats.birthtime.getTime(), children: [], }); } else { result.push({ title: file, type: 'file', key, parent: relativePath, size: stats.size, createTime: stats.birthtime.getTime(), }); } } return result; } catch (error: any) { if (error.code === 'ENOENT') { return []; } throw error; } } export async function promiseExec(command: string): Promise { try { const { stderr, stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8', }); return stdout || stderr; } catch (error) { return JSON.stringify(error); } } export async function promiseExecSuccess(command: string): Promise { try { const { stdout } = await promisify(exec)(command, { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8', }); return stdout || ''; } catch (error) { return ''; } } export function parseHeaders(headers: string) { if (!headers) return {}; const parsed: any = {}; let key: string; let val: string; let i: number; headers && headers.split('\n').forEach(function parser(line) { i = line.indexOf(':'); key = line.substring(0, i).trim().toLowerCase(); val = line.substring(i + 1).trim(); if (!key) { return; } parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; }); return parsed; } function parseString( input: string, valueFormatFn?: (v: string) => string, ): Record { const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g; const matches: Record = {}; let match; while ((match = regex.exec(input)) !== null) { const [, key, value] = match; const _key = key.trim(); if (!_key || matches[_key]) { continue; } let _value = value.trim(); try { _value = valueFormatFn ? valueFormatFn(_value) : _value; const jsonValue = JSON.parse(_value); matches[_key] = jsonValue; } catch (error) { matches[_key] = _value; } } return matches; } export function parseBody( body: string, contentType: | 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' | 'text/plain', valueFormatFn?: (v: string) => string, ) { if (contentType === 'text/plain' || !body) { return valueFormatFn && body ? valueFormatFn(body) : body; } const parsed = parseString(body, valueFormatFn); switch (contentType) { case 'multipart/form-data': return Object.keys(parsed).reduce((p, c) => { p.append(c, parsed[c]); return p; }, new FormData()); case 'application/x-www-form-urlencoded': return Object.keys(parsed).reduce((p, c) => { return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`; }); } return parsed; } export function psTree(pid: number): Promise { return new Promise((resolve, reject) => { psTreeFun(pid, (err: any, children) => { if (err) { reject(err); } resolve(children.map((x) => Number(x.PID)).filter((x) => !isNaN(x))); }); }); } export async function killTask(pid: number, waitForExit = false) { const descendants = await psTree(pid); const signal = (target: number, sig: NodeJS.Signals) => { try { process.kill(target, sig); } catch (error: any) { if (error.code !== 'ESRCH') throw error; } }; if (!waitForExit) { if (descendants.length) { // A child may exit after psTree; keep signalling the remaining tree. for (const target of [pid, ...descendants].reverse()) { signal(target, 'SIGTERM'); } } else signal(pid, 'SIGINT'); return; } const pids = [...descendants.reverse(), pid]; for (const target of pids) signal(target, 'SIGTERM'); const alive = async (target: number) => { try { process.kill(target, 0); } catch (error: any) { if (error.code === 'ESRCH') return false; throw error; } if (process.platform === 'linux') { try { const stat = await fs.readFile(`/proc/${target}/stat`, 'utf8'); // The command field may contain spaces and parentheses. Zombies have // exited even while their parent has not reaped the PID yet. const state = stat.slice(stat.lastIndexOf(')') + 2).split(' ')[0]; if (['Z', 'X', 'x'].includes(state)) return false; } catch { // /proc may be unavailable, or the process may have just exited. // Retain the portable signal probe rather than assuming it is dead. try { process.kill(target, 0); } catch (error: any) { if (error.code === 'ESRCH') return false; throw error; } } } return true; }; const wait = async () => { const deadline = Date.now() + 1000; let remaining = pids; while (true) { const states = await Promise.all(remaining.map(alive)); remaining = remaining.filter((_, index) => states[index]); if (!remaining.length || Date.now() >= deadline) return remaining; await new Promise((resolve) => setTimeout(resolve, 20)); } }; let remaining = await wait(); if (!remaining.length) return; for (const target of remaining) signal(target, 'SIGKILL'); remaining = await wait(); if (remaining.length) throw new Error(`Task processes did not exit: ${remaining.join(', ')}`); } export async function getPid(cmd: string) { const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`; const pid = await promiseExec(taskCommand); return pid ? Number(pid) : undefined; } export async function getAllPids(cmd: string): Promise { const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}'`; const pidsStr = await promiseExec(taskCommand); if (!pidsStr) return []; return pidsStr .split('\n') .map((p) => Number(p.trim())) .filter((p) => !isNaN(p) && p > 0); } export async function killAllTasks(cmd: string): Promise { const pids = await getAllPids(cmd); for (const pid of pids) { try { await killTask(pid); } catch (error) { // Ignore errors if process already terminated } } } interface IVersion { version: string; changeLogLink: string; changeLog: string; publishTime: string; } export async function parseVersion(path: string): Promise { return load(await fs.readFile(path, 'utf8')) as IVersion; } export function parseContentVersion(content: string): IVersion { return load(content) as IVersion; } export async function getUniqPath( command: string, id: string, ): Promise { let suffix = ''; if (/^\d+$/.test(id)) { suffix = `_${id}`; } let items = command.split(/ +/); const maxTimeCommandIndex = items.findIndex((x) => x === '-m'); if (maxTimeCommandIndex !== -1) { items = items.slice(maxTimeCommandIndex + 2); } let str = items[0]; if (items[0] === TASK_COMMAND) { str = items[1]; } const dotIndex = str.lastIndexOf('.'); if (dotIndex !== -1) { str = str.slice(0, dotIndex); } const slashIndex = str.lastIndexOf('/'); let tempStr = ''; if (slashIndex !== -1) { tempStr = str.slice(0, slashIndex); const _slashIndex = tempStr.lastIndexOf('/'); if (_slashIndex !== -1) { tempStr = tempStr.slice(_slashIndex + 1); } str = `${tempStr}_${str.slice(slashIndex + 1)}`; } return `${str}${suffix}`; } export function safeJSONParse(value?: string) { if (!value) { return {}; } try { return JSON.parse(value); } catch (error) { Logger.error('[safeJSONParse error]', error); return {}; } } export { errStack } from '../shared/errors'; export async function rmPath(path: string) { try { const _exsit = await fileExist(path); if (_exsit) { await fs.rm(path, { force: true, recursive: true, maxRetries: 5 }); } } catch (error) { Logger.error('[rmPath error]', error); } } export async function setSystemTimezone(timezone: string): Promise { try { if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) { throw new Error('Invalid timezone'); } await promiseExec( maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`), ); await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`); return true; } catch (error) { Logger.error('[setSystemTimezone error]', error); return false; } } // 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 isPythonSourceDependency(name: string): boolean { const source = name.trim(); return ( isGitHubUrl(source) || isRequirementsFile(source) || isPyprojectToml(source) ); } 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 (isPythonSourceDependency(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, PackageNotFoundError import importlib.util import sys try: try: from packaging.requirements import Requirement except ImportError: from pip._vendor.packaging.requirements import Requirement requirement = Requirement(sys.argv[1]) if requirement.marker and not requirement.marker.evaluate(): print('not applicable') else: try: installed = version(requirement.name) except PackageNotFoundError: if not requirement.specifier and not requirement.extras and not requirement.url: if importlib.util.find_spec(requirement.name): print(requirement.name) else: if requirement.specifier.contains(installed, prereleases=True): print(installed) except Exception: pass`, )} ${shellQuote(trimmedName)}`, [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' ? `apk info -es ${trimmedName}` : maybeSudo(`dpkg-query -s ${trimmedName}`), }; 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'), }; return `${baseCommands[type]} ${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.', )}`; } const packageName = name.match( /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]+\])?(?=\s*(?:[<>=!~;@]|$))/, )?.[1] || name; return `${getPythonPip()} uninstall --disable-pip-version-check${ PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore' } -y -- ${shellQuote(packageName)}`; } const baseCommands = { [DependenceTypes.nodejs]: 'pnpm remove -g', [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' ? 'apk del' : maybeSudo('apt-get remove -y'), }; return `${baseCommands[type]} ${name.trim()}`; } export function isDemoEnv() { return process.env.DeployEnv === 'demo'; } async function getOSReleaseInfo(): Promise { const osRelease = await fs.readFile('/etc/os-release', 'utf8'); return osRelease; } function isDebian(osReleaseInfo: string): boolean { return osReleaseInfo.includes('Debian'); } function isUbuntu(osReleaseInfo: string): boolean { return osReleaseInfo.includes('Ubuntu'); } function isCentOS(osReleaseInfo: string): boolean { return osReleaseInfo.includes('CentOS') || osReleaseInfo.includes('Red Hat'); } function isAlpine(osReleaseInfo: string): boolean { return osReleaseInfo.includes('Alpine'); } export async function detectOS(): Promise< 'Debian' | 'Ubuntu' | 'Alpine' | undefined > { if (osType) return osType; const envOs = process.env.QL_OS_TYPE?.toLowerCase(); if (envOs === 'alpine') { osType = 'Alpine'; return osType; } if (envOs === 'debian') { osType = 'Debian'; return osType; } if (envOs === 'ubuntu') { osType = 'Ubuntu'; return osType; } const platform = os.platform(); if (platform === 'linux') { const osReleaseInfo = await getOSReleaseInfo(); if (isDebian(osReleaseInfo)) { osType = 'Debian'; } else if (isUbuntu(osReleaseInfo)) { osType = 'Ubuntu'; } else if (isAlpine(osReleaseInfo)) { osType = 'Alpine'; } else { Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`); console.error(`Unknown Linux Distribution: ${osReleaseInfo}`); } } else if (platform === 'darwin') { osType = undefined; } else { Logger.error(`Unsupported platform: ${platform}`); console.error(`Unsupported platform: ${platform}`); } return osType; } async function _updateLinuxMirror( osType: string, mirror: string, ): Promise { const defaults: Record = { 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 = osType !== 'Alpine' && 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`; } } } 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 { const detectedOS = await detectOS(); if (!detectedOS) { throw Error(`Unknown Linux Distribution`); } return await _updateLinuxMirror(detectedOS, mirror); }