import * as plugins from './mod.plugins.js'; import * as paths from '../paths.js'; import type { IFormatOperation } from './interfaces.format.js'; export class RollbackManager { private backupDir: string; private manifestPath: string; constructor() { this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups'); this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json'); } async createOperation(): Promise { await this.ensureBackupDir(); const operation: IFormatOperation = { id: this.generateOperationId(), timestamp: Date.now(), files: [], status: 'pending', }; await this.updateManifest(operation); return operation; } async backupFile(filepath: string, operationId: string): Promise { const operation = await this.getOperation(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } const absolutePath = plugins.path.isAbsolute(filepath) ? filepath : plugins.path.join(paths.cwd, filepath); // Check if file exists const exists = await plugins.smartfile.fs.fileExists(absolutePath); if (!exists) { // File doesn't exist yet (will be created), so we skip backup return; } // Read file content and metadata const content = plugins.smartfile.fs.toStringSync(absolutePath); const stats = await plugins.smartfile.fs.stat(absolutePath); const checksum = this.calculateChecksum(content); // Create backup const backupPath = this.getBackupPath(operationId, filepath); await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath)); await plugins.smartfile.memory.toFs(content, backupPath); // Update operation operation.files.push({ path: filepath, originalContent: content, checksum, permissions: stats.mode.toString(8), }); await this.updateManifest(operation); } async rollback(operationId: string): Promise { const operation = await this.getOperation(operationId); if (!operation) { // Operation doesn't exist, might have already been rolled back or never created console.warn(`Operation ${operationId} not found for rollback, skipping`); return; } if (operation.status === 'rolled-back') { throw new Error(`Operation ${operationId} has already been rolled back`); } // Restore files in reverse order for (let i = operation.files.length - 1; i >= 0; i--) { const file = operation.files[i]; const absolutePath = plugins.path.isAbsolute(file.path) ? file.path : plugins.path.join(paths.cwd, file.path); // Verify backup integrity const backupPath = this.getBackupPath(operationId, file.path); const backupContent = plugins.smartfile.fs.toStringSync(backupPath); const backupChecksum = this.calculateChecksum(backupContent); if (backupChecksum !== file.checksum) { throw new Error(`Backup integrity check failed for ${file.path}`); } // Restore file await plugins.smartfile.memory.toFs(file.originalContent, absolutePath); // Restore permissions const mode = parseInt(file.permissions, 8); // Note: Permissions restoration may not work on all platforms } // Update operation status operation.status = 'rolled-back'; await this.updateManifest(operation); } async markComplete(operationId: string): Promise { const operation = await this.getOperation(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.status = 'completed'; await this.updateManifest(operation); } async cleanOldBackups(retentionDays: number): Promise { const manifest = await this.getManifest(); const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000; const operationsToDelete = manifest.operations.filter( (op) => op.timestamp < cutoffTime && op.status === 'completed', ); for (const operation of operationsToDelete) { // Remove backup files const operationDir = plugins.path.join( this.backupDir, 'operations', operation.id, ); await plugins.smartfile.fs.remove(operationDir); // Remove from manifest manifest.operations = manifest.operations.filter( (op) => op.id !== operation.id, ); } await this.saveManifest(manifest); } async verifyBackup(operationId: string): Promise { const operation = await this.getOperation(operationId); if (!operation) { return false; } for (const file of operation.files) { const backupPath = this.getBackupPath(operationId, file.path); const exists = await plugins.smartfile.fs.fileExists(backupPath); if (!exists) { return false; } const content = plugins.smartfile.fs.toStringSync(backupPath); const checksum = this.calculateChecksum(content); if (checksum !== file.checksum) { return false; } } return true; } async listBackups(): Promise { const manifest = await this.getManifest(); return manifest.operations; } private async ensureBackupDir(): Promise { await plugins.smartfile.fs.ensureDir(this.backupDir); await plugins.smartfile.fs.ensureDir( plugins.path.join(this.backupDir, 'operations'), ); } private generateOperationId(): string { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const random = Math.random().toString(36).substring(2, 8); return `${timestamp}-${random}`; } private getBackupPath(operationId: string, filepath: string): string { const filename = plugins.path.basename(filepath); const dir = plugins.path.dirname(filepath); const safeDir = dir.replace(/[/\\]/g, '__'); return plugins.path.join( this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`, ); } private calculateChecksum(content: string | Buffer): string { return plugins.crypto.createHash('sha256').update(content).digest('hex'); } private async getManifest(): Promise<{ operations: IFormatOperation[] }> { const defaultManifest = { operations: [] }; const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); if (!exists) { return defaultManifest; } try { const content = plugins.smartfile.fs.toStringSync(this.manifestPath); const manifest = JSON.parse(content); // Validate the manifest structure if (this.isValidManifest(manifest)) { return manifest; } else { console.warn( 'Invalid rollback manifest structure, returning default manifest', ); return defaultManifest; } } catch (error) { console.warn( `Failed to read rollback manifest: ${error.message}, returning default manifest`, ); // Try to delete the corrupted file try { await plugins.smartfile.fs.remove(this.manifestPath); } catch (removeError) { // Ignore removal errors } return defaultManifest; } } private async saveManifest(manifest: { operations: IFormatOperation[]; }): Promise { // Validate before saving if (!this.isValidManifest(manifest)) { throw new Error('Invalid rollback manifest structure, cannot save'); } // Ensure directory exists await this.ensureBackupDir(); // Write directly with proper JSON stringification const jsonContent = JSON.stringify(manifest, null, 2); await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath); } private async getOperation( operationId: string, ): Promise { const manifest = await this.getManifest(); return manifest.operations.find((op) => op.id === operationId) || null; } private async updateManifest(operation: IFormatOperation): Promise { const manifest = await this.getManifest(); const existingIndex = manifest.operations.findIndex( (op) => op.id === operation.id, ); if (existingIndex !== -1) { manifest.operations[existingIndex] = operation; } else { manifest.operations.push(operation); } await this.saveManifest(manifest); } private isValidManifest( manifest: any, ): manifest is { operations: IFormatOperation[] } { // Check if manifest has the required structure if (!manifest || typeof manifest !== 'object') { return false; } // Check required fields if (!Array.isArray(manifest.operations)) { return false; } // Check each operation entry for (const operation of manifest.operations) { if ( !operation || typeof operation !== 'object' || typeof operation.id !== 'string' || typeof operation.timestamp !== 'number' || typeof operation.status !== 'string' || !Array.isArray(operation.files) ) { return false; } // Check each file in the operation for (const file of operation.files) { if ( !file || typeof file !== 'object' || typeof file.path !== 'string' || typeof file.checksum !== 'string' ) { return false; } } } return true; } }