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
This commit is contained in:
kaitranntt committed 2026-02-03 19:48:00 -05:00
1 parent b833b149e0
commit c324e92eb4
2 files changed
+14 -3

No files matched your search

+7 -1
View File
@@ -123,13 +123,19 @@ export function ensureProfileHooks(profileName: string): boolean {
// Clean up any duplicates that may have accumulated (Windows path bug fix) // Clean up any duplicates that may have accumulated (Windows path bug fix)
const hadDuplicates = deduplicateCcsHooks(settings); const hadDuplicates = deduplicateCcsHooks(settings);
if (hadDuplicates) { if (hadDuplicates) {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); // 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) { if (process.env.CCS_DEBUG) {
console.error( console.error(
info(`Removed duplicate WebSearch hooks from ${profileName}.settings.json`) info(`Removed duplicate WebSearch hooks from ${profileName}.settings.json`)
); );
} }
} }
}
// Update timeout if needed // Update timeout if needed
return updateHookTimeoutIfNeeded(settings, settingsPath); return updateHookTimeoutIfNeeded(settings, settingsPath);
} }
+7 -2
View File
@@ -156,9 +156,13 @@ 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; let backupPath: string | undefined;
const newContent = JSON.stringify(settings, null, 2) + '\n';
if (fileExists) { if (fileExists) {
const existingContent = fs.readFileSync(settingsPath, 'utf8');
// Only create backup if content differs
if (existingContent !== newContent) {
const backupDir = path.join(ccsDir, 'backups'); const backupDir = path.join(ccsDir, 'backups');
if (!fs.existsSync(backupDir)) { if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true }); fs.mkdirSync(backupDir, { recursive: true });
@@ -167,6 +171,7 @@ router.put('/:profile', (req: Request, res: Response): void => {
backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`); backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`);
fs.copyFileSync(settingsPath, backupPath); fs.copyFileSync(settingsPath, backupPath);
} }
}
// Ensure directory exists for new files // Ensure directory exists for new files
if (!fileExists) { if (!fileExists) {
@@ -175,7 +180,7 @@ router.put('/:profile', (req: Request, res: Response): void => {
// Write new settings atomically // Write new settings atomically
const tempPath = settingsPath + '.tmp'; const tempPath = settingsPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n'); fs.writeFileSync(tempPath, newContent);
fs.renameSync(tempPath, settingsPath); fs.renameSync(tempPath, settingsPath);
const newStat = fs.statSync(settingsPath); const newStat = fs.statSync(settingsPath);