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
2026-02-03 19:48:00 -05:00
parent b833b149e0
commit c324e92eb4
2 changed files with 24 additions and 13 deletions
+11 -5
View File
@@ -123,11 +123,17 @@ 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)
if (process.env.CCS_DEBUG) { const newContent = JSON.stringify(settings, null, 2);
console.error( const existingContent = fs.readFileSync(settingsPath, 'utf8');
info(`Removed duplicate WebSearch hooks from ${profileName}.settings.json`) // 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 // Update timeout if needed
+13 -8
View File
@@ -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; let backupPath: string | undefined;
const newContent = JSON.stringify(settings, null, 2) + '\n';
if (fileExists) { if (fileExists) {
const backupDir = path.join(ccsDir, 'backups'); const existingContent = fs.readFileSync(settingsPath, 'utf8');
if (!fs.existsSync(backupDir)) { // Only create backup if content differs
fs.mkdirSync(backupDir, { recursive: true }); 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 // Ensure directory exists for new files
@@ -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);