Add multi-OS support for Linux package mirror configuration#2920
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Import updateLinuxMirrorFile function to support Debian, Ubuntu, and Alpine - Add OS detection logic (detectOS, getOSReleaseInfo, isDebian, isUbuntu, isAlpine) - Add mirror domain extraction and replacement functions - Update SystemService.updateLinuxMirror to use new multi-OS implementation - Save config only if mirror update succeeds (hasError flag) - Support different source files: /etc/apt/sources.list.d for Debian/Ubuntu, /etc/apk/repositories for Alpine Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
- Add error handling for file read operations (getOSReleaseInfo, getCurrentMirrorDomain) - Fix trailing slash bug: add slash before replacement, not after - Escape special regex characters in domain names to prevent incorrect replacements - Fix OS detection order: check Ubuntu before Debian to avoid misidentification - Add escapeRegExp helper function for regex escaping Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
|
@MonkeyCode-AI review 一下 |
|
MonkeyCode-AI 正在分析任务... |
MonkeyCode-AI
left a comment
There was a problem hiding this comment.
我是 MonkeyCode AI 编程助手,你可以在 GitHub 仓库的 PR 中 at @MonkeyCode-AI 来呼唤我。
任务执行细节请参考: https://monkeycode-ai.com/tasks/public?id=e409b48e-170f-4467-8d6f-3216c4f70abb
代码审查结果
整体方向正确,但对 URL 规范化、替换精度与 apt 源文件(尤其 deb822 .sources)兼容性风险较高,可能导致替换失败或生成错误 URL,需修改后再合并。
✨ 代码亮点
- 将镜像更新逻辑从 Alpine 专用 sed 迁移到可扩展的 OS 路由结构,方向正确
- 使用 escapeRegExp 规避正则替换中的特殊字符问题
- 仅在镜像更新成功后再持久化配置,降低写入错误配置的风险
| 🚨 Critical | 💡 Suggestion | |
|---|---|---|
| 2 | 2 | 0 |
| // 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); |
There was a problem hiding this comment.
Caution
🚨 对 newDomain 强制追加 '/' 可能生成错误的仓库 URL(双斜杠或破坏 apt URI 字段)
replaceDomainInFile() 在替换前强制给 newDomainWithScheme 追加 trailing slash。getCurrentMirrorDomain() 返回形如 http(s)://host(不含尾斜杠),但实际源文件中被替换位置常见为 http://host/path... 或 deb822 .sources 的 URIs: http://host/debian。
- 若原文本为
http://deb.debian.org/debian,替换为http://mirror/会得到http://mirror//debian。 - 对 deb822 的
URIs:,追加/可能造成不一致甚至解析问题(取决于 apt 版本/格式)。
更稳妥的是不人为追加/,仅替换域名部分并保持原有路径分隔符。
建议: 移除强制追加 / 的逻辑,只替换域名(含 scheme + host)部分;或用更精确的正则捕获 scheme://host 并替换为同形态的 scheme://newhost,不改变路径分隔符数量。
| // 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 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; | ||
| } |
There was a problem hiding this comment.
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: 的行提取域名,并对匹配域名集合逐一替换而非只取第一个。
| 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}`); | ||
| } |
There was a problem hiding this comment.
Warning
detectOS() 在未知发行版/不支持平台时同时调用 Logger.error 和 console.error,容易导致重复输出、格式不一致,并且在日志收集系统中不易统一检索。服务端通常应统一使用 Logger。
建议: 移除 console.error,仅保留 Logger.error;或在 Logger 内部统一处理 console 输出。
| 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}`); | |
| } |
| 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.'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Warning
_updateLinuxMirror(osType: string, ...) 接收 string,但实际只应为 'Debian'|'Ubuntu'|'Alpine'。这降低编译期检查能力,允许非法值流入并增加运行时错误面,也让 switch 的 default 在类型层面“可达”。既然 detectOS() 已返回联合类型,建议贯穿使用该类型。
建议: 将 _updateLinuxMirror 的 osType 参数收紧为联合类型,并在调用链中保持该类型,减少运行时错误。
| 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.`); | |
| } | |
| } | |
| } |
7539d75 to
e8ac195
Compare
f43efe3 to
7933fe0
Compare
aa0f902 to
0aaaee2
Compare
0169ebf to
d301643
Compare
23a1835 to
4ebc049
Compare
509b0d0 to
da6dac8
Compare
152a9aa to
92e4f26
Compare
9b37f06 to
0bb41eb
Compare
de65d7f to
5f6049d
Compare
Linux mirror configuration only worked on Alpine, failing for Debian/Ubuntu containers with malformed repository URLs.
Changes
OS Detection (
back/config/util.ts)detectOS(): Identifies Debian, Ubuntu, or Alpine via/etc/os-releaseMirror Configuration (
back/config/util.ts)getCurrentMirrorDomain(): Extracts current mirror URL from configreplaceDomainInFile(): Updates mirror with regex escaping for special charsupdateLinuxMirrorFile(): Entry point routing to OS-specific handlersService Update (
back/services/system.ts)updateLinuxMirror(): Now usesupdateLinuxMirrorFile()instead of Alpine-only sed commandsOS-Specific Behavior
/etc/apk/repositoriesapk updatehttp://dl-cdn.alpinelinux.org/etc/apt/sources.list.d/debian.sourcesapt-get updatehttp://deb.debian.org/etc/apt/sources.list.d/ubuntu.sourcesapt-get updatehttp://archive.ubuntu.comPorted from existing
debianbranch implementation that was never merged to master.Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.