fix(format): Improve concurrency control in cache and rollback management with mutex locking and refine formatting details
This commit is contained in:
@@ -5,206 +5,227 @@ 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'
|
||||
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
|
||||
|
||||
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)
|
||||
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`);
|
||||
// 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
|
||||
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<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'
|
||||
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);
|
||||
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);
|
||||
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 = 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'));
|
||||
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`);
|
||||
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');
|
||||
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`);
|
||||
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);
|
||||
@@ -214,85 +235,84 @@ export class RollbackManager {
|
||||
return defaultManifest;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> {
|
||||
|
||||
private async saveManifest(manifest: {
|
||||
operations: IFormatOperation[];
|
||||
}): Promise<void> {
|
||||
// Validate before saving
|
||||
if (!this.isValidManifest(manifest)) {
|
||||
throw new Error('Invalid rollback manifest structure, cannot save');
|
||||
}
|
||||
|
||||
// Use atomic write: write to temp file, then move it
|
||||
const tempPath = `${this.manifestPath}.tmp`;
|
||||
|
||||
try {
|
||||
// Write to temporary file
|
||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
||||
await plugins.smartfile.memory.toFs(jsonContent, tempPath);
|
||||
|
||||
// Move temp file to actual manifest (atomic-like operation)
|
||||
// Since smartfile doesn't have rename, we copy and delete
|
||||
await plugins.smartfile.fs.copy(tempPath, this.manifestPath);
|
||||
await plugins.smartfile.fs.remove(tempPath);
|
||||
} catch (error) {
|
||||
// Clean up temp file if it exists
|
||||
try {
|
||||
await plugins.smartfile.fs.remove(tempPath);
|
||||
} catch (removeError) {
|
||||
// Ignore removal errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 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<IFormatOperation | null> {
|
||||
|
||||
private async getOperation(
|
||||
operationId: string,
|
||||
): Promise<IFormatOperation | null> {
|
||||
const manifest = await this.getManifest();
|
||||
return manifest.operations.find(op => op.id === operationId) || null;
|
||||
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);
|
||||
|
||||
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[] } {
|
||||
|
||||
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)) {
|
||||
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') {
|
||||
if (
|
||||
!file ||
|
||||
typeof file !== 'object' ||
|
||||
typeof file.path !== 'string' ||
|
||||
typeof file.checksum !== 'string'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user