From ab9aecd7d5c91b8be974bda4f20094b214fd1c1d Mon Sep 17 00:00:00 2001 From: choibk Date: Mon, 26 Jan 2026 08:30:52 +0900 Subject: [PATCH] fix(system): decouple update process into standalone detached script --- server/routes/system.js | 116 ++++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/server/routes/system.js b/server/routes/system.js index 2e25ad6..4955a9d 100644 --- a/server/routes/system.js +++ b/server/routes/system.js @@ -588,46 +588,106 @@ router.post('/version/update', isAuthenticated, hasRole('admin'), async (req, re return res.status(400).json({ error: '업데이트할 대상 태그가 지정되지 않았습니다.' }); } - // This operation is asynchronous. We start it and return a message. - // In a real production, we might want to log this to a terminal-like view. - const auth = await getGiteaAuth(); - let authPrefix = ''; + const env = readEnv(); + const isWindows = process.platform === 'win32'; + const backupDir = isWindows ? './backup' : '/volume1/backup/smart_ims'; + + // Build auth URL for git commands + let remoteUrl = auth.url; if (auth.user && auth.pass) { - const authenticatedUrl = auth.url.replace('https://', `https://${encodeURIComponent(auth.user)}:${encodeURIComponent(auth.pass)}@`); - authPrefix = `git remote set-url origin ${authenticatedUrl} && `; - } else { - authPrefix = `git remote set-url origin ${auth.url} && `; + remoteUrl = auth.url.replace('https://', `https://${encodeURIComponent(auth.user)}:${encodeURIComponent(auth.pass)}@`); } - const isWindows = process.platform === 'win32'; + const timestamp = new Date().toISOString().replace(/[:T]/g, '-').split('.')[0]; + const dbName = env.DB_NAME || 'sokuree_platform_prod'; + const dbUser = env.DB_USER || 'choibk'; + const dbPass = env.DB_PASSWORD || ''; + const dbPort = env.DB_PORT || '3307'; - // Build a more robust update script - // On dev environments, pm2 might fail, so we make it optional (|| echo) - const updateScript = isWindows - ? `${authPrefix} git fetch --tags --force && git checkout -f ${targetTag} && npm install && npm run build` - : `${authPrefix} git fetch --tags --force && git checkout -f ${targetTag} && npm install && npm run build && cd server && npm install && pm2 reload smartims-api || echo "PM2 not found, skipping reload"`; + // 1. Generate Standalone Update Script Content + let scriptContent = ''; + const scriptName = isWindows ? 'update_system.bat' : 'update_system.sh'; + const scriptPath = path.join(__dirname, '../..', scriptName); - const shellCommand = isWindows ? `cmd.exe /c "${updateScript}"` : updateScript; + if (isWindows) { + scriptContent = ` +@echo off +echo [Update] Starting update to ${targetTag}... +if not exist "${backupDir}" mkdir "${backupDir}" - console.log(`🚀 Starting system update to ${targetTag}...`); - console.log(`Executing: ${shellCommand}`); +echo [Update] Backing up Uploads... +tar -czvf "${backupDir}/backup_images_${timestamp}.tar.gz" server/uploads/ - exec(shellCommand, { cwd: path.join(__dirname, '../..') }, (err, stdout, stderr) => { - if (err) { - console.error('❌ Update Failed:', err); - // Sanitize output for logs - const sanitizedErr = stderr.replace(/:[^@]+@/g, ':****@'); - console.error(sanitizedErr); - return; +echo [Update] Syncing Source Code... +git fetch "${remoteUrl}" +refs/tags/*:refs/tags/* --force --prune --prune-tags +git checkout -f ${targetTag} + +echo [Update] Installing & Building... +call npm install +call npm run build +cd server +call npm install + +echo [Update] Restarting Server... +echo "Please restart your dev server manually if needed." + `; + } else { + // Linux/Synology Script + const dumpTool = '/usr/local/mariadb10/bin/mysqldump'; + scriptContent = `#!/bin/bash +exec > >(tee -a update.log) 2>&1 +echo "[Update] Starting update to ${targetTag}..." +mkdir -p ${backupDir} + +echo "[Update] Backing up Database..." +${dumpTool} -u ${dbUser} --password='${dbPass}' --port ${dbPort} ${dbName} > ${backupDir}/backup_db_${timestamp}.sql || echo "DB Backup Failed, continuing..." + +echo "[Update] Backing up Uploads..." +tar -czvf ${backupDir}/backup_images_${timestamp}.tar.gz server/uploads/ + +echo "[Update] Syncing Source Code..." +git remote set-url origin "${remoteUrl}" +git fetch origin +refs/tags/*:refs/tags/* --force --prune --prune-tags +git checkout -f ${targetTag} + +echo "[Update] Installing & Building..." +npm install +npm run build +cd server +npm install + +echo "[Update] Reloading PM2..." +pm2 reload smartims-api || echo "PM2 not found" +echo "[Update] Done." + `; + } + + // 2. Write Script to File + try { + fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf8', mode: 0o755 }); + } catch (err) { + console.error('Failed to create update script:', err); + return res.status(500).json({ error: '업데이트 스크립트 생성 실패' }); + } + + // 3. Execute Script Detached + console.log(`🚀 [Update] Spawning independent update process: ${scriptPath}`); + + const child = require('child_process').spawn( + isWindows ? 'cmd.exe' : 'bash', + isWindows ? ['/c', scriptName] : [scriptName], + { + cwd: path.join(__dirname, '../..'), + detached: true, + stdio: 'ignore' } - console.log('✅ Update completed successfully.'); - console.log(stdout); - }); + ); + child.unref(); // Allow node process to exit/reload without killing this child res.json({ success: true, - message: '업데이트 프로세스가 백그라운드에서 시작되었습니다. 약 1~3분 후 시스템이 재시작됩니다.' + message: '업데이트 프로세스가 독립적으로 실행되었습니다. 시스템이 곧 재시작됩니다.' }); });