Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions back/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence';
import { FormData } from 'undici';
import os from 'os';

export * from './share';

Expand Down Expand Up @@ -590,3 +591,162 @@ export function getUninstallCommand(
export function isDemoEnv() {
return process.env.DeployEnv === 'demo';
}

// OS detection for Linux mirror configuration
let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined;

async function getOSReleaseInfo(): Promise<string> {
try {
const osRelease = await fs.readFile('/etc/os-release', 'utf8');
return osRelease;
} catch (error) {
Logger.error(`Failed to read /etc/os-release: ${error}`);
return '';
}
}

function isDebian(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Debian');
}

function isUbuntu(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Ubuntu');
}

function isAlpine(osReleaseInfo: string): boolean {
return osReleaseInfo.includes('Alpine');
}

export async function detectOS(): Promise<
'Debian' | 'Ubuntu' | 'Alpine' | undefined
> {
if (osType) return osType;
const platform = os.platform();

if (platform === 'linux') {
const osReleaseInfo = await getOSReleaseInfo();
// Check Ubuntu before Debian since Ubuntu is based on Debian
if (isUbuntu(osReleaseInfo)) {
osType = 'Ubuntu';
} else if (isDebian(osReleaseInfo)) {
osType = 'Debian';
} 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}`);
}
Comment on lines +636 to +644
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

⚠️ 库代码同时使用 Logger.error 与 console.error,输出重复且不利于统一日志管理

detectOS() 在未知发行版/不支持平台时同时调用 Logger.error 和 console.error,容易导致重复输出、格式不一致,并且在日志收集系统中不易统一检索。服务端通常应统一使用 Logger。

建议: 移除 console.error,仅保留 Logger.error;或在 Logger 内部统一处理 console 输出。

Suggested change
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}`);
}
} else {
Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
}
} else if (platform === 'darwin') {
osType = undefined;
} else {
Logger.error(`Unsupported platform: ${platform}`);
}


return osType;
}

async function getCurrentMirrorDomain(
filePath: string,
): Promise<string | null> {
try {
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;
} catch (error) {
Logger.error(`Failed to read mirror configuration file ${filePath}: ${error}`);
return null;
}
Comment on lines +649 to +668
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

🚨 getCurrentMirrorDomain() 仅提取首个 https?://host,对 deb822 .sources 与多 URI/多行场景不可靠

Debian/Ubuntu 的 /etc/apt/sources.list.d/*.sources 通常是 deb822 格式,可能包含多个 URIs:、安全源、updates、ports、或多行字段。当前实现逐行扫描并返回第一个非注释行内匹配到的 https?://[^/]+
风险:

  • 文件内存在多个域名(如 security 源)时,只会将“第一个匹配到的域名”作为 currentDomain,随后 replaceDomainInFile() 全文替换该域名,导致仅该域名被改,其它域名保持不变。
  • 若首个匹配来自第三方源/非主仓库,会把它当作 currentDomain,从而替换错误对象。
    结果可能是“看似替换成功但 apt update 失败/仍访问旧源”。

建议: 针对 deb822 .sources 解析 URIs: 字段并替换其 host(可替换所有 URIs 列表);或至少在 Debian/Ubuntu 分支优先从包含 URIs: 的行提取域名,并对匹配域名集合逐一替换而非只取第一个。

}

function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

async function replaceDomainInFile(
filePath: string,
oldDomainWithScheme: string,
newDomainWithScheme: string,
): Promise<void> {
// Ensure the new domain has a trailing slash before replacement
if (!newDomainWithScheme.endsWith('/')) {
newDomainWithScheme += '/';
}

let fileContent = await fs.readFile(filePath, 'utf8');
// Escape special regex characters in the old domain
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
let updatedContent = fileContent.replace(
new RegExp(escapedOldDomain, 'g'),
newDomainWithScheme,
);

await writeFileWithLock(filePath, updatedContent);
Comment on lines +680 to +693
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

🚨 对 newDomain 强制追加 '/' 可能生成错误的仓库 URL(双斜杠或破坏 apt URI 字段)

replaceDomainInFile() 在替换前强制给 newDomainWithScheme 追加 trailing slash。getCurrentMirrorDomain() 返回形如 http(s)://host(不含尾斜杠),但实际源文件中被替换位置常见为 http://host/path... 或 deb822 .sourcesURIs: http://host/debian

  • 若原文本为 http://deb.debian.org/debian,替换为 http://mirror/ 会得到 http://mirror//debian
  • 对 deb822 的 URIs:,追加 / 可能造成不一致甚至解析问题(取决于 apt 版本/格式)。
    更稳妥的是不人为追加 /,仅替换域名部分并保持原有路径分隔符。

建议: 移除强制追加 / 的逻辑,只替换域名(含 scheme + host)部分;或用更精确的正则捕获 scheme://host 并替换为同形态的 scheme://newhost,不改变路径分隔符数量。

Suggested change
// Ensure the new domain has a trailing slash before replacement
if (!newDomainWithScheme.endsWith('/')) {
newDomainWithScheme += '/';
}
let fileContent = await fs.readFile(filePath, 'utf8');
// Escape special regex characters in the old domain
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
let updatedContent = fileContent.replace(
new RegExp(escapedOldDomain, 'g'),
newDomainWithScheme,
);
await writeFileWithLock(filePath, updatedContent);
async function replaceDomainInFile(
filePath: string,
oldDomainWithScheme: string,
newDomainWithScheme: string,
): Promise<void> {
const fileContent = await fs.readFile(filePath, 'utf8');
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
const updatedContent = fileContent.replace(
new RegExp(escapedOldDomain, 'g'),
newDomainWithScheme,
);
await writeFileWithLock(filePath, updatedContent);
}

}

async function _updateLinuxMirror(
osType: string,
mirrorDomainWithScheme: string,
): Promise<string> {
let filePath: string, currentDomainWithScheme: string | null;
switch (osType) {
case 'Debian':
filePath = '/etc/apt/sources.list.d/debian.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://deb.debian.org',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Ubuntu':
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Alpine':
filePath = '/etc/apk/repositories';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
);
return 'apk update';
} else {
throw Error(`Current mirror domain not found.`);
}
default:
throw Error('Unsupported OS type for updating mirrors.');
}
}
Comment on lines +696 to +744
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

⚠️ _updateLinuxMirror() 的 osType 参数为 string,丢失类型约束且 default 分支不可达但仍存在

_updateLinuxMirror(osType: string, ...) 接收 string,但实际只应为 'Debian'|'Ubuntu'|'Alpine'。这降低编译期检查能力,允许非法值流入并增加运行时错误面,也让 switch 的 default 在类型层面“可达”。既然 detectOS() 已返回联合类型,建议贯穿使用该类型。

建议: 将 _updateLinuxMirror 的 osType 参数收紧为联合类型,并在调用链中保持该类型,减少运行时错误。

Suggested change
async function _updateLinuxMirror(
osType: string,
mirrorDomainWithScheme: string,
): Promise<string> {
let filePath: string, currentDomainWithScheme: string | null;
switch (osType) {
case 'Debian':
filePath = '/etc/apt/sources.list.d/debian.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://deb.debian.org',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Ubuntu':
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Alpine':
filePath = '/etc/apk/repositories';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
);
return 'apk update';
} else {
throw Error(`Current mirror domain not found.`);
}
default:
throw Error('Unsupported OS type for updating mirrors.');
}
}
async function _updateLinuxMirror(
osType: 'Debian' | 'Ubuntu' | 'Alpine',
mirrorDomainWithScheme: string,
): Promise<string> {
let filePath: string, currentDomainWithScheme: string | null;
switch (osType) {
case 'Debian':
filePath = '/etc/apt/sources.list.d/debian.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://deb.debian.org',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Ubuntu':
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
);
return 'apt-get update';
} else {
throw Error(`Current mirror domain not found.`);
}
case 'Alpine':
filePath = '/etc/apk/repositories';
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
if (currentDomainWithScheme) {
await replaceDomainInFile(
filePath,
currentDomainWithScheme,
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
);
return 'apk update';
} else {
throw Error(`Current mirror domain not found.`);
}
}
}


export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
const detectedOS = await detectOS();
if (!detectedOS) {
throw Error(`Unknown Linux Distribution`);
}
return await _updateLinuxMirror(detectedOS, mirror);
}
34 changes: 10 additions & 24 deletions back/services/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
readDirs,
rmPath,
setSystemTimezone,
updateLinuxMirrorFile,
} from '../config/util';
import {
DependenceModel,
Expand Down Expand Up @@ -214,33 +215,11 @@ export default class SystemService {
onEnd?: () => void,
) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org';
if (os.platform() !== 'linux') {
return;
}
const content = await fs.promises.readFile('/etc/apk/repositories', {
encoding: 'utf-8',
});
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
if (domainMatch) {
defaultDomain = domainMatch[1];
}
if (info.linuxMirror) {
targetDomain = info.linuxMirror;
}
const command = `sed -i 's/${defaultDomain.replace(
/\//g,
'\\/',
)}/${targetDomain.replace(
/\//g,
'\\/',
)}/g' /etc/apk/repositories && apk update -f`;

const command = await updateLinuxMirrorFile(info.linuxMirror || '');
let hasError = false;
this.scheduleService.runTask(
command,
{
Expand All @@ -254,8 +233,15 @@ export default class SystemService {
message: 'update linux mirror end',
});
onEnd?.();
if (!hasError) {
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
}
},
onError: async (message: string) => {
hasError = true;
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
onLog: async (message: string) => {
Expand Down