UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

377 lines (376 loc) 14.7 kB
/** * Atomic File Writer * * Provides atomic file write operations with SHA256 checksum verification, * rollback capability, and permission preservation. * Part of Task 4.2: Centralized File Locking & Atomic Operations * * Features: * - Write-then-move pattern (write to temp, verify, move to target) * - SHA256 checksum verification * - Automatic rollback on failure * - Preserve file permissions and ownership * - Backup creation before overwrite * - Integration with file lock manager * * Usage: * const writer = new AtomicFileWriter(); * await writer.writeFile('/path/to/file.txt', content, { * createBackup: true, * preservePermissions: true * }); */ import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; import { createLogger } from './logging.js'; import { createError, ErrorCode } from './errors.js'; import { withFileLock } from './file-lock-manager.js'; const logger = createLogger('atomic-file-writer'); const fsWriteFile = promisify(fs.writeFile); const fsReadFile = promisify(fs.readFile); const fsRename = promisify(fs.rename); const fsUnlink = promisify(fs.unlink); const fsStat = promisify(fs.stat); const fsMkdir = promisify(fs.mkdir); const fsChmod = promisify(fs.chmod); const fsChown = promisify(fs.chown); const fsAccess = promisify(fs.access); const fsCopyFile = promisify(fs.copyFile); /** * Atomic File Writer * * Provides safe atomic file writes with verification and rollback. */ export class AtomicFileWriter { /** * Write file atomically with verification * * @param filePath - Target file path * @param content - Content to write (string or Buffer) * @param options - Write options * @returns Promise<WriteResult> - Write result with metadata */ async writeFile(filePath, content, options = {}) { const startTime = Date.now(); const absolutePath = path.resolve(filePath); const opts = { encoding: options.encoding || 'utf8', createBackup: options.createBackup || false, preservePermissions: options.preservePermissions !== false, preserveOwnership: options.preserveOwnership || false, verifyChecksum: options.verifyChecksum !== false, useLock: options.useLock !== false, lockOptions: options.lockOptions || {}, backupDir: options.backupDir }; logger.debug('Starting atomic write', { filePath: absolutePath, contentLength: typeof content === 'string' ? content.length : content.length, options: opts }); // Calculate expected checksum const expectedChecksum = this.calculateChecksum(content); // Execute write with lock if requested if (opts.useLock) { return withFileLock(absolutePath, async ()=>this.performWrite(absolutePath, content, opts, expectedChecksum, startTime), opts.lockOptions); } else { return this.performWrite(absolutePath, content, opts, expectedChecksum, startTime); } } /** * Perform the actual write operation */ async performWrite(absolutePath, content, options, expectedChecksum, startTime) { const dir = path.dirname(absolutePath); const tempPath = path.join(dir, `.${path.basename(absolutePath)}.${randomUUID()}.tmp`); let backupPath; let originalMetadata = null; let rolledBack = false; try { // Ensure directory exists await this.ensureDirectory(dir); // Get original file metadata (if exists) originalMetadata = await this.getFileMetadata(absolutePath); // Create backup if requested and file exists if (options.createBackup && originalMetadata.exists) { backupPath = await this.createBackup(absolutePath, options.backupDir); logger.info('Backup created', { original: absolutePath, backup: backupPath }); } // Write to temporary file logger.debug('Writing to temporary file', { tempPath }); await fsWriteFile(tempPath, content, options.encoding); // Verify checksum if (options.verifyChecksum) { const actualChecksum = await this.calculateFileChecksum(tempPath); if (actualChecksum !== expectedChecksum) { throw createError(ErrorCode.CHECKSUM_MISMATCH, 'Checksum verification failed after write', { expected: expectedChecksum, actual: actualChecksum, filePath: absolutePath }); } logger.debug('Checksum verified', { checksum: actualChecksum }); } // Preserve permissions if requested if (options.preservePermissions && originalMetadata.exists) { await fsChmod(tempPath, originalMetadata.mode); logger.debug('Permissions preserved', { mode: originalMetadata.mode.toString(8) }); } // Preserve ownership if requested (requires privileges) if (options.preserveOwnership && originalMetadata.exists) { try { await fsChown(tempPath, originalMetadata.uid, originalMetadata.gid); logger.debug('Ownership preserved', { uid: originalMetadata.uid, gid: originalMetadata.gid }); } catch (error) { logger.warn('Failed to preserve ownership (requires elevated privileges)', error instanceof Error ? error : undefined); } } // Atomic move to target location logger.debug('Moving to target location', { from: tempPath, to: absolutePath }); await fsRename(tempPath, absolutePath); // Get final file size const stat = await fsStat(absolutePath); const bytesWritten = stat.size; const durationMs = Date.now() - startTime; logger.info('Atomic write completed successfully', { filePath: absolutePath, bytesWritten, checksum: expectedChecksum, durationMs }); return { success: true, filePath: absolutePath, checksum: expectedChecksum, bytesWritten, durationMs, backupPath, rolledBack: false }; } catch (error) { logger.error('Atomic write failed, attempting rollback', error instanceof Error ? error : undefined, { filePath: absolutePath }); // Attempt rollback try { // Remove temp file if it exists if (await this.fileExists(tempPath)) { await fsUnlink(tempPath); logger.debug('Temporary file removed', { tempPath }); } // Restore from backup if available if (backupPath && await this.fileExists(backupPath)) { await fsCopyFile(backupPath, absolutePath); logger.info('Restored from backup after failed write', { backup: backupPath, target: absolutePath }); rolledBack = true; } } catch (rollbackError) { logger.error('Rollback failed', rollbackError instanceof Error ? rollbackError : undefined, { filePath: absolutePath, backupPath }); } const durationMs = Date.now() - startTime; throw createError(ErrorCode.FILE_WRITE_FAILED, `Atomic write failed: ${absolutePath}`, { filePath: absolutePath, durationMs, backupPath, rolledBack }, error instanceof Error ? error : undefined); } } /** * Calculate SHA256 checksum of content */ calculateChecksum(content) { const hash = crypto.createHash('sha256'); hash.update(content); return hash.digest('hex'); } /** * Calculate SHA256 checksum of file */ async calculateFileChecksum(filePath) { return new Promise((resolve, reject)=>{ const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('data', (chunk)=>hash.update(chunk)); stream.on('end', ()=>resolve(hash.digest('hex'))); stream.on('error', reject); }); } /** * Get file metadata for permission preservation */ async getFileMetadata(filePath) { try { const stats = await fsStat(filePath); return { mode: stats.mode, uid: stats.uid, gid: stats.gid, exists: true }; } catch (error) { // File doesn't exist return { mode: 0o644, uid: process.getuid ? process.getuid() : 0, gid: process.getgid ? process.getgid() : 0, exists: false }; } } /** * Create backup of existing file */ async createBackup(filePath, backupDir) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const basename = path.basename(filePath); const targetDir = backupDir || path.dirname(filePath); // Ensure backup directory exists await this.ensureDirectory(targetDir); const backupPath = path.join(targetDir, `${basename}.${timestamp}.backup`); try { await fsCopyFile(filePath, backupPath); return backupPath; } catch (error) { throw createError(ErrorCode.BACKUP_FAILED, `Failed to create backup: ${filePath}`, { filePath, backupPath }, error instanceof Error ? error : undefined); } } /** * Check if file exists */ async fileExists(filePath) { try { await fsAccess(filePath, fs.constants.F_OK); return true; } catch { return false; } } /** * Ensure directory exists */ async ensureDirectory(dirPath) { try { await fsMkdir(dirPath, { recursive: true }); } catch (error) { // Ignore error if directory already exists if (error.code !== 'EEXIST') { throw error; } } } /** * Read file atomically with checksum verification * * @param filePath - File to read * @param expectedChecksum - Optional expected checksum * @param encoding - File encoding (default: 'utf8') * @returns Promise<{content: string, checksum: string}> - File content and checksum */ async readFile(filePath, expectedChecksum, encoding = 'utf8') { const absolutePath = path.resolve(filePath); try { const content = await fsReadFile(absolutePath, encoding); const checksum = this.calculateChecksum(content); if (expectedChecksum && checksum !== expectedChecksum) { throw createError(ErrorCode.CHECKSUM_MISMATCH, 'Checksum verification failed during read', { expected: expectedChecksum, actual: checksum, filePath: absolutePath }); } logger.debug('File read with checksum verification', { filePath: absolutePath, checksum, verified: !!expectedChecksum }); return { content, checksum }; } catch (error) { throw createError(ErrorCode.FILE_READ_FAILED, `Failed to read file: ${absolutePath}`, { filePath: absolutePath }, error instanceof Error ? error : undefined); } } /** * Verify file checksum * * @param filePath - File to verify * @param expectedChecksum - Expected SHA256 checksum * @returns Promise<boolean> - True if checksum matches */ async verifyChecksum(filePath, expectedChecksum) { const absolutePath = path.resolve(filePath); try { const actualChecksum = await this.calculateFileChecksum(absolutePath); const matches = actualChecksum === expectedChecksum; logger.debug('Checksum verification', { filePath: absolutePath, expected: expectedChecksum, actual: actualChecksum, matches }); return matches; } catch (error) { logger.error('Checksum verification failed', error instanceof Error ? error : undefined, { filePath: absolutePath }); return false; } } } /** * Singleton instance */ let defaultWriter = null; /** * Get the default atomic file writer instance */ export function getAtomicFileWriter() { if (!defaultWriter) { defaultWriter = new AtomicFileWriter(); } return defaultWriter; } /** * Helper function for atomic writes * * @param filePath - Target file path * @param content - Content to write * @param options - Write options * @returns Promise<WriteResult> - Write result */ export async function atomicWriteFile(filePath, content, options = {}) { const writer = getAtomicFileWriter(); return writer.writeFile(filePath, content, options); } /** * Helper function for atomic reads with checksum * * @param filePath - File to read * @param expectedChecksum - Optional expected checksum * @param encoding - File encoding * @returns Promise<{content: string, checksum: string}> */ export async function atomicReadFile(filePath, expectedChecksum, encoding = 'utf8') { const writer = getAtomicFileWriter(); return writer.readFile(filePath, expectedChecksum, encoding); } //# sourceMappingURL=atomic-file-writer.js.map