From c324e92eb442669656b53a8f685030f5cb15ce3d Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 3 Feb 2026 15:49:10 -0500 Subject: [PATCH] fix(backup): create backups only when settings content changes Previously, PUT /api/settings/:profile created a backup on every request regardless of whether the content actually changed. This led to hundreds of identical backup files accumulating in ~/.ccs/backups/. Changes: - Compare existing content with new content before creating backup - Reuse computed newContent for atomic write (DRY) - Make hook injection idempotent by checking content before write Fixes #433 --- src/utils/websearch/profile-hook-injector.ts | 16 ++++++++++----- src/web-server/routes/settings-routes.ts | 21 ++++++++++++-------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/utils/websearch/profile-hook-injector.ts b/src/utils/websearch/profile-hook-injector.ts index 5a98b00c..8b2916de 100644 --- a/src/utils/websearch/profile-hook-injector.ts +++ b/src/utils/websearch/profile-hook-injector.ts @@ -123,11 +123,17 @@ export function ensureProfileHooks(profileName: string): boolean { // Clean up any duplicates that may have accumulated (Windows path bug fix) const hadDuplicates = deduplicateCcsHooks(settings); if (hadDuplicates) { - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); - if (process.env.CCS_DEBUG) { - console.error( - info(`Removed duplicate WebSearch hooks from ${profileName}.settings.json`) - ); + // Re-read file to compare with modified settings (deduplicateCcsHooks mutates in-place) + const newContent = JSON.stringify(settings, null, 2); + const existingContent = fs.readFileSync(settingsPath, 'utf8'); + // Only write if content actually changed + if (newContent !== existingContent) { + fs.writeFileSync(settingsPath, newContent, 'utf8'); + if (process.env.CCS_DEBUG) { + console.error( + info(`Removed duplicate WebSearch hooks from ${profileName}.settings.json`) + ); + } } } // Update timeout if needed diff --git a/src/web-server/routes/settings-routes.ts b/src/web-server/routes/settings-routes.ts index 57b61aad..baaa73a9 100644 --- a/src/web-server/routes/settings-routes.ts +++ b/src/web-server/routes/settings-routes.ts @@ -156,16 +156,21 @@ router.put('/:profile', (req: Request, res: Response): void => { } } - // Create backup only if file exists + // Create backup only if file exists AND content actually changed let backupPath: string | undefined; + const newContent = JSON.stringify(settings, null, 2) + '\n'; if (fileExists) { - const backupDir = path.join(ccsDir, 'backups'); - if (!fs.existsSync(backupDir)) { - fs.mkdirSync(backupDir, { recursive: true }); + const existingContent = fs.readFileSync(settingsPath, 'utf8'); + // Only create backup if content differs + if (existingContent !== newContent) { + const backupDir = path.join(ccsDir, 'backups'); + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + } + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`); + fs.copyFileSync(settingsPath, backupPath); } - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`); - fs.copyFileSync(settingsPath, backupPath); } // Ensure directory exists for new files @@ -175,7 +180,7 @@ router.put('/:profile', (req: Request, res: Response): void => { // Write new settings atomically const tempPath = settingsPath + '.tmp'; - fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n'); + fs.writeFileSync(tempPath, newContent); fs.renameSync(tempPath, settingsPath); const newStat = fs.statSync(settingsPath);