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 = await 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) { throw new Error(`Operation ${operationId} not found`); } 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 = await 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 = await 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 exists = await plugins.smartfile.fs.fileExists(this.manifestPath); if (!exists) { return { operations: [] }; } const content = await plugins.smartfile.fs.toStringSync(this.manifestPath); return JSON.parse(content); } private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise { await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), 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); } }