From aa52cfb29dc29abe2037c676f0e94ae8f3574d8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:26:57 +0000 Subject: [PATCH 1/3] Initial plan From d43d5636224484386e46e756cc908188239a3617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:36:48 +0000 Subject: [PATCH 2/3] Add multi-OS support for Linux package mirror configuration - 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> --- back/config/util.ts | 142 ++++++++++++++++++++++++++++++++++++++++ back/services/system.ts | 34 +++------- 2 files changed, 152 insertions(+), 24 deletions(-) diff --git a/back/config/util.ts b/back/config/util.ts index 7d61f74f..6b56b659 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -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'; @@ -590,3 +591,144 @@ 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 { + 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 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(); + 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 getCurrentMirrorDomain( + filePath: string, +): Promise { + 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 { + let fileContent = await fs.readFile(filePath, 'utf8'); + let updatedContent = fileContent.replace( + new RegExp(oldDomainWithScheme, 'g'), + newDomainWithScheme, + ); + + if (!newDomainWithScheme.endsWith('/')) { + newDomainWithScheme += '/'; + } + + await writeFileWithLock(filePath, updatedContent); +} + +async function _updateLinuxMirror( + osType: string, + mirrorDomainWithScheme: string, +): Promise { + 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.'); + } +} + +export async function updateLinuxMirrorFile(mirror: string): Promise { + const detectedOS = await detectOS(); + if (!detectedOS) { + throw Error(`Unknown Linux Distribution`); + } + return await _updateLinuxMirror(detectedOS, mirror); +} diff --git a/back/services/system.ts b/back/services/system.ts index ecc2a732..9467b429 100644 --- a/back/services/system.ts +++ b/back/services/system.ts @@ -17,6 +17,7 @@ import { readDirs, rmPath, setSystemTimezone, + updateLinuxMirrorFile, } from '../config/util'; import { DependenceModel, @@ -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, { @@ -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) => { From c329c8acd4a62d81918e2910105dcfcd36c3dce4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:38:54 +0000 Subject: [PATCH 3/3] Address code review feedback - 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> --- back/config/util.ts | 60 +++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/back/config/util.ts b/back/config/util.ts index 6b56b659..796025b9 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -596,8 +596,13 @@ export function isDemoEnv() { let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined; async function getOSReleaseInfo(): Promise { - const osRelease = await fs.readFile('/etc/os-release', 'utf8'); - return osRelease; + 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 { @@ -620,10 +625,11 @@ export async function detectOS(): Promise< if (platform === 'linux') { const osReleaseInfo = await getOSReleaseInfo(); - if (isDebian(osReleaseInfo)) { - osType = 'Debian'; - } else if (isUbuntu(osReleaseInfo)) { + // 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 { @@ -643,18 +649,27 @@ export async function detectOS(): Promise< async function getCurrentMirrorDomain( filePath: string, ): Promise { - 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]; + 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; } - return null; +} + +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } async function replaceDomainInFile( @@ -662,16 +677,19 @@ async function replaceDomainInFile( oldDomainWithScheme: string, newDomainWithScheme: string, ): Promise { - let fileContent = await fs.readFile(filePath, 'utf8'); - let updatedContent = fileContent.replace( - new RegExp(oldDomainWithScheme, 'g'), - newDomainWithScheme, - ); - + // 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); }