218 lines
7.2 KiB
TypeScript
218 lines
7.2 KiB
TypeScript
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<IFormatOperation> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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<IFormatOperation[]> {
|
|
const manifest = await this.getManifest();
|
|
return manifest.operations;
|
|
}
|
|
|
|
private async ensureBackupDir(): Promise<void> {
|
|
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<void> {
|
|
await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath);
|
|
}
|
|
|
|
private async getOperation(operationId: string): Promise<IFormatOperation | null> {
|
|
const manifest = await this.getManifest();
|
|
return manifest.operations.find(op => op.id === operationId) || null;
|
|
}
|
|
|
|
private async updateManifest(operation: IFormatOperation): Promise<void> {
|
|
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);
|
|
}
|
|
} |