basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as fs from 'node:fs';8 9/**10 * Options for writeWithBackup function.11 */12export interface WriteWithBackupOptions {13 /** Suffix for backup file (default: '.orig') */14 backupSuffix?: string;15 /** File encoding (default: 'utf-8') */16 encoding?: BufferEncoding;17}18 19/**20 * Safely writes content to a file with backup protection.21 *22 * This function ensures data safety by:23 * 1. Writing content to a temporary file first24 * 2. Backing up the existing target file (if any)25 * 3. Renaming the temporary file to the target path26 *27 * If any step fails, an error is thrown and no partial changes are left on disk.28 * The backup acts as an in-flight safety net: if the final rename fails the29 * original is restored from it. On success the backup is removed so it does not30 * linger next to the target file (which would pollute the user's project).31 *32 * Note: This is not 100% atomic but provides good protection. In the worst case33 * (a crash between the backup rename and the final rename), a .orig file remains34 * holding the last good content and can be manually restored.35 *36 * @param targetPath - The path to write to37 * @param content - The content to write38 * @param options - Optional configuration39 * @throws Error if any step of the write process fails40 *41 * @example42 * ```typescript43 * await writeWithBackup('/path/to/settings.json', JSON.stringify(settings, null, 2));44 * // On success only /path/to/settings.json exists; the .orig backup is cleaned up.45 * ```46 */47export async function writeWithBackup(48 targetPath: string,49 content: string,50 options: WriteWithBackupOptions = {},51): Promise<void> {52 // Async version delegates to sync version since file operations are synchronous53 writeWithBackupSync(targetPath, content, options);54}55 56/**57 * Synchronous version of writeWithBackup.58 *59 * @param targetPath - The path to write to60 * @param content - The content to write61 * @param options - Optional configuration62 * @throws Error if any step of the write process fails63 */64export function writeWithBackupSync(65 targetPath: string,66 content: string,67 options: WriteWithBackupOptions = {},68): void {69 const { backupSuffix = '.orig', encoding = 'utf-8' } = options;70 const tempPath = `${targetPath}.tmp`;71 const backupPath = `${targetPath}${backupSuffix}`;72 let backupCreated = false;73 74 // Clean up any existing temp file from previous failed attempts75 try {76 if (fs.existsSync(tempPath)) {77 fs.unlinkSync(tempPath);78 }79 } catch (_e) {80 // Ignore cleanup errors81 }82 83 try {84 // Step 1: Write to temporary file85 fs.writeFileSync(tempPath, content, { encoding, flush: true });86 87 // Step 2: If target exists, back it up88 if (fs.existsSync(targetPath)) {89 // Check if target is a directory - we can't write to a directory90 const targetStat = fs.statSync(targetPath);91 if (targetStat.isDirectory()) {92 // Clean up temp file before throwing93 try {94 fs.unlinkSync(tempPath);95 } catch (_e) {96 // Ignore cleanup error97 }98 throw new Error(99 `Cannot write to '${targetPath}' because it is a directory`,100 );101 }102 103 try {104 fs.renameSync(targetPath, backupPath);105 backupCreated = true;106 } catch (backupError) {107 // Clean up temp file before throwing108 try {109 fs.unlinkSync(tempPath);110 } catch (_e) {111 // Ignore cleanup error112 }113 throw new Error(114 `Failed to backup existing file: ${backupError instanceof Error ? backupError.message : String(backupError)}`,115 );116 }117 }118 119 // Step 3: Rename temp file to target120 try {121 fs.renameSync(tempPath, targetPath);122 123 // Step 4: Write succeeded — the backup was only an in-flight safety net,124 // so remove it instead of leaving a .orig file behind in the user's dir.125 if (backupCreated) {126 try {127 fs.unlinkSync(backupPath);128 } catch (_e) {129 // Best-effort cleanup; a stray backup is harmless and non-fatal.130 }131 }132 } catch (renameError) {133 let restoreFailedMessage: string | undefined;134 let backupExisted = false;135 136 // Attempt to restore backup if rename failed137 if (fs.existsSync(backupPath)) {138 backupExisted = true;139 try {140 fs.renameSync(backupPath, targetPath);141 } catch (restoreError) {142 restoreFailedMessage =143 restoreError instanceof Error144 ? restoreError.message145 : String(restoreError);146 }147 }148 149 const writeFailureMessage =150 renameError instanceof Error151 ? renameError.message152 : String(renameError);153 154 if (restoreFailedMessage) {155 throw new Error(156 `Failed to write file: ${writeFailureMessage}. ` +157 `Automatic restore failed: ${restoreFailedMessage}. ` +158 `Manual recovery may be required using backup file '${backupPath}'.`,159 );160 }161 162 if (backupExisted) {163 throw new Error(164 `Failed to write file: ${writeFailureMessage}. ` +165 `Target was automatically restored from backup '${backupPath}'.`,166 );167 }168 169 throw new Error(170 `Failed to write file: ${writeFailureMessage}. No backup file was available for restoration.`,171 );172 }173 } catch (error) {174 // Ensure temp file is cleaned up on any error175 try {176 if (fs.existsSync(tempPath)) {177 fs.unlinkSync(tempPath);178 }179 } catch (_e) {180 // Ignore cleanup error181 }182 throw error;183 }184}185 