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.
779 lines (778 loc) • 31.4 kB
JavaScript
/**
* Unified Backup & Restore Manager
*
* Centralized backup and restore system for all critical file operations.
* Part of Task 4.3: Unified Backup & Restore System
*
* Features:
* - Multiple backup types (pre-edit, checkpoint, manual)
* - SQLite metadata storage with queryability
* - Restore operations (latest, by timestamp, by hash)
* - Restore verification with hash comparison
* - Dry-run mode for restore preview
* - Automatic rollback on verification failure
* - Rate limiting for restore operations
* - Comprehensive audit trail
* - Disk usage monitoring
* - Automatic cleanup based on TTL
* - Integration with FileLockManager
*
* Usage:
* const manager = new BackupManager();
* const backup = await manager.createBackup('/path/to/file.txt', {
* agentId: 'backend-dev-001',
* backupType: 'pre-edit'
* });
*
* // Restore latest backup
* await manager.restoreLatest('/path/to/file.txt', {
* agentId: 'backend-dev-001',
* verify: 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 Database from 'better-sqlite3';
import { createLogger } from './logging.js';
import { createError, ErrorCode, StandardError } from './errors.js';
import { getFileLockManager } from './file-lock-manager.js';
import { withFileSystemRetry } from './retry-manager.js';
import { getEncryptionManager } from './encryption-manager.js';
const logger = createLogger('backup-manager');
const fsReadFile = promisify(fs.readFile);
const fsWriteFile = promisify(fs.writeFile);
const fsCopyFile = promisify(fs.copyFile);
const fsStat = promisify(fs.stat);
const fsMkdir = promisify(fs.mkdir);
const fsAccess = promisify(fs.access);
const fsReaddir = promisify(fs.readdir);
const fsUnlink = promisify(fs.unlink);
/**
* Backup type classification
*/ export var BackupType = /*#__PURE__*/ function(BackupType) {
BackupType["PRE_EDIT"] = "pre-edit";
BackupType["CHECKPOINT"] = "checkpoint";
BackupType["MANUAL"] = "manual";
return BackupType;
}({});
/**
* Unified Backup & Restore Manager
*/ export class BackupManager {
db;
lockManager;
encryptionManager;
backupDir;
defaultTtlMs;
rateLimitConfig;
projectRoot;
constructor(config = {}){
this.projectRoot = config.projectRoot || process.cwd();
this.backupDir = config.backupDir || path.join(this.projectRoot, '.backups');
this.defaultTtlMs = config.defaultTtlMs || 24 * 60 * 60 * 1000; // 24 hours
this.rateLimitConfig = config.rateLimit || {
maxRestoresPerHour: 100
};
const dbPath = config.dbPath || path.join(this.projectRoot, 'claude-assets/skills/cfn-redis-coordination/data/backups.db');
// Initialize database
this.db = this.initializeDatabase(dbPath);
// Initialize file lock manager
this.lockManager = getFileLockManager();
// Initialize encryption manager (CVSS 7.2 mitigation)
this.encryptionManager = getEncryptionManager();
// Ensure backup directory exists
this.ensureBackupDirectory();
logger.info('Backup manager initialized', {
backupDir: this.backupDir,
dbPath,
defaultTtlMs: this.defaultTtlMs,
encryptionEnabled: this.encryptionManager.isEnabled()
});
}
/**
* Create a backup of a file
*
* @param filePath - Path to file to backup
* @param options - Backup options
* @returns Backup instance
*/ async createBackup(filePath, options) {
const startTime = Date.now();
const absolutePath = path.resolve(filePath);
logger.info('Creating backup', {
filePath: absolutePath,
agentId: options.agentId,
backupType: options.backupType
});
// Acquire file lock
const lock = await this.lockManager.acquireLock(absolutePath, {
agentId: options.agentId,
timeout: 30000
});
try {
// Check if file exists
const exists = await this.fileExists(absolutePath);
if (!exists) {
throw createError(ErrorCode.FILE_NOT_FOUND, `File does not exist: ${absolutePath}`, {
filePath: absolutePath
});
}
// Read file and calculate hash
const fileContent = await fsReadFile(absolutePath);
const originalHash = this.calculateHash(fileContent);
const fileSize = fileContent.length;
// Create backup directory structure
const timestamp = Date.now();
const backupPath = this.getBackupPath(options.agentId, timestamp, originalHash);
await this.ensureDirectory(path.dirname(backupPath));
// Copy file to backup location with retry logic for transient failures
await withFileSystemRetry(async ()=>{
await fsCopyFile(absolutePath, backupPath);
});
// Verify backup with retry logic
const backupContent = await withFileSystemRetry(async ()=>{
return await fsReadFile(backupPath);
});
const backupHash = this.calculateHash(backupContent);
if (!this.constantTimeHashCompare(originalHash, backupHash)) {
// Cleanup failed backup
await this.safeUnlink(backupPath);
throw createError(ErrorCode.VALIDATION_FAILED, 'Backup verification failed: hash mismatch', {
originalHash,
backupHash,
filePath: absolutePath
});
}
// Calculate expiration
const ttlMs = options.ttlMs || this.defaultTtlMs;
const expiresAt = new Date(Date.now() + ttlMs);
// Store metadata in database
const backupId = randomUUID();
const backup = {
id: backupId,
filePath: absolutePath,
backupPath,
agentId: options.agentId,
backupType: options.backupType,
originalHash,
backupHash,
fileSize,
createdAt: new Date(),
expiresAt,
metadata: options.metadata
};
// ENCRYPTION SUPPORT (CVSS 7.2 mitigation)
let encryptionMetadata = null;
if (this.encryptionManager.isEnabled()) {
try {
const encrypted = await this.encryptionManager.encrypt(backupContent, backupId);
encryptionMetadata = encrypted.metadata;
// Write encrypted backup
await withFileSystemRetry(async ()=>{
await fsWriteFile(backupPath, encrypted.data);
});
logger.info('Backup encrypted successfully', {
backupId,
algorithm: encrypted.metadata.algorithm,
originalSize: backupContent.length,
encryptedSize: encrypted.data.length
});
} catch (encryptError) {
logger.error('Backup encryption failed', encryptError instanceof Error ? encryptError : undefined, {
backupId,
filePath: absolutePath
});
// Cleanup failed backup
await this.safeUnlink(backupPath);
throw encryptError;
}
}
this.insertBackup(backup, encryptionMetadata);
const duration = Date.now() - startTime;
logger.info('Backup created successfully', {
backupId,
filePath: absolutePath,
backupPath,
fileSize,
durationMs: duration
});
return backup;
} catch (error) {
const duration = Date.now() - startTime;
logger.error('Backup creation failed', error instanceof Error ? error : undefined, {
filePath: absolutePath,
durationMs: duration
});
// Log failed backup to audit trail
this.logAuditEntry({
backupId: null,
operation: 'create',
agentId: options.agentId,
status: 'failure',
filePath: absolutePath,
errorMessage: error instanceof Error ? error.message : String(error),
errorCode: error instanceof StandardError ? error.code : ErrorCode.UNKNOWN_ERROR,
durationMs: duration
});
throw error;
} finally{
await this.lockManager.releaseLock(lock.id);
}
}
/**
* Restore the latest backup for a file
*
* @param filePath - Path to file to restore
* @param options - Restore options
* @returns Restore result
*/ async restoreLatest(filePath, options) {
const absolutePath = path.resolve(filePath);
const backup = this.getLatestBackup(absolutePath);
if (!backup) {
throw createError(ErrorCode.FILE_NOT_FOUND, `No backup found for: ${absolutePath}`, {
filePath: absolutePath
});
}
return this.restoreBackup(backup.id, options);
}
/**
* Restore backup by timestamp
*
* @param filePath - Path to file
* @param timestamp - Backup timestamp
* @param options - Restore options
* @returns Restore result
*/ async restoreByTimestamp(filePath, timestamp, options) {
const absolutePath = path.resolve(filePath);
const backup = this.getBackupByTimestamp(absolutePath, timestamp);
if (!backup) {
throw createError(ErrorCode.FILE_NOT_FOUND, `No backup found for: ${absolutePath} at timestamp ${timestamp.toISOString()}`, {
filePath: absolutePath,
timestamp: timestamp.toISOString()
});
}
return this.restoreBackup(backup.id, options);
}
/**
* Restore backup by hash
*
* @param filePath - Path to file
* @param hash - File hash
* @param options - Restore options
* @returns Restore result
*/ async restoreByHash(filePath, hash, options) {
const absolutePath = path.resolve(filePath);
const backup = this.getBackupByHash(absolutePath, hash);
if (!backup) {
throw createError(ErrorCode.FILE_NOT_FOUND, `No backup found for: ${absolutePath} with hash ${hash}`, {
filePath: absolutePath,
hash
});
}
return this.restoreBackup(backup.id, options);
}
/**
* Restore a specific backup by ID
*
* @param backupId - Backup ID
* @param options - Restore options
* @returns Restore result
*/ async restoreBackup(backupId, options) {
const startTime = Date.now();
const verify = options.verify !== false;
const dryRun = options.dryRun || false;
const force = options.force || false;
const createBackupBeforeRestore = options.createBackupBeforeRestore !== false;
logger.info('Restoring backup', {
backupId,
agentId: options.agentId,
verify,
dryRun
});
// Get backup metadata
const metadata = this.getBackupMetadata(backupId);
if (!metadata) {
throw createError(ErrorCode.FILE_NOT_FOUND, `Backup not found: ${backupId}`, {
backupId
});
}
// Check rate limit
if (!force && !dryRun) {
const rateLimitOk = this.checkRateLimit(options.agentId);
if (!rateLimitOk) {
throw createError(ErrorCode.LOCK_TIMEOUT, 'Restore rate limit exceeded', {
agentId: options.agentId,
maxRestoresPerHour: this.rateLimitConfig.maxRestoresPerHour
});
}
}
// Verify backup file exists
const backupExists = await this.fileExists(metadata.backupPath);
if (!backupExists) {
throw createError(ErrorCode.FILE_NOT_FOUND, `Backup file not found: ${metadata.backupPath}`, {
backupId,
backupPath: metadata.backupPath
});
}
// Dry-run mode: just verify and return
if (dryRun) {
const backupContent = await fsReadFile(metadata.backupPath);
const verificationHash = this.calculateHash(backupContent);
return {
success: true,
backupId,
filePath: metadata.filePath,
backupPath: metadata.backupPath,
verified: this.constantTimeHashCompare(verificationHash, metadata.backupHash),
dryRun: true,
restoredAt: new Date(),
verificationHash,
expectedHash: metadata.backupHash
};
}
// Acquire file lock
const lock = await this.lockManager.acquireLock(metadata.filePath, {
agentId: options.agentId,
timeout: 30000
});
let rollbackBackupId;
try {
// Create backup of current file before restore
if (createBackupBeforeRestore && await this.fileExists(metadata.filePath)) {
const rollbackBackup = await this.createBackup(metadata.filePath, {
agentId: options.agentId,
backupType: "pre-edit",
metadata: {
reason: 'pre-restore-backup',
restoringBackupId: backupId
}
});
rollbackBackupId = rollbackBackup.id;
}
// DECRYPTION SUPPORT (CVSS 7.2 mitigation)
let backupDataToRestore;
if (metadata.isEncrypted && this.encryptionManager.isEnabled()) {
try {
const encryptedBackupContent = await withFileSystemRetry(async ()=>{
return await fsReadFile(metadata.backupPath);
});
const encryptedPayload = {
data: encryptedBackupContent,
metadata: {
algorithm: 'AES-256-GCM',
iv: metadata.encryptionIv,
authTag: metadata.encryptionAuthTag,
hmac: metadata.encryptionHmac,
encryptedAt: metadata.encryptedAt,
keyVersion: metadata.encryptionKeyVersion
}
};
const decryptionResult = await this.encryptionManager.decrypt(encryptedPayload, backupId);
if (!decryptionResult.integrityVerified) {
logger.warn('Backup integrity check failed during restore', {
backupId,
integrityVerified: false
});
}
backupDataToRestore = decryptionResult.data;
logger.info('Backup decrypted successfully', {
backupId,
integrityVerified: decryptionResult.integrityVerified
});
} catch (decryptError) {
logger.error('Backup decryption failed', decryptError instanceof Error ? decryptError : undefined, {
backupId,
filePath: metadata.filePath
});
throw decryptError;
}
} else {
// Load unencrypted backup
backupDataToRestore = await withFileSystemRetry(async ()=>{
return await fsReadFile(metadata.backupPath);
});
}
// Perform restore with retry logic for transient failures
await withFileSystemRetry(async ()=>{
await fsWriteFile(metadata.filePath, backupDataToRestore);
});
// Verify restore if requested
let verified = false;
let verificationHash;
if (verify) {
const restoredContent = await withFileSystemRetry(async ()=>{
return await fsReadFile(metadata.filePath);
});
verificationHash = this.calculateHash(restoredContent);
verified = this.constantTimeHashCompare(verificationHash, metadata.originalHash);
if (!verified) {
// Verification failed - rollback if we created a backup
let rollbackPerformed = false;
if (rollbackBackupId) {
try {
await this.restoreBackup(rollbackBackupId, {
agentId: options.agentId,
verify: false,
dryRun: false,
force: true,
createBackupBeforeRestore: false
});
rollbackPerformed = true;
} catch (rollbackError) {
logger.error('Rollback failed after verification failure', rollbackError instanceof Error ? rollbackError : undefined, {
backupId,
rollbackBackupId
});
}
}
throw createError(ErrorCode.VALIDATION_FAILED, 'Restore verification failed: hash mismatch', {
backupId,
filePath: metadata.filePath,
verificationHash,
expectedHash: metadata.originalHash,
rollbackPerformed
});
}
}
// Record restore in rate limit table
this.recordRestore(backupId, options.agentId, metadata.filePath);
// Log successful restore to audit trail
const duration = Date.now() - startTime;
this.logAuditEntry({
backupId,
operation: 'restore',
agentId: options.agentId,
status: 'success',
filePath: metadata.filePath,
backupPath: metadata.backupPath,
durationMs: duration,
metadata: {
verified,
rollbackBackupId
}
});
logger.info('Restore completed successfully', {
backupId,
filePath: metadata.filePath,
verified,
durationMs: duration
});
return {
success: true,
backupId,
filePath: metadata.filePath,
backupPath: metadata.backupPath,
verified,
dryRun: false,
restoredAt: new Date(),
verificationHash,
expectedHash: metadata.originalHash
};
} catch (error) {
const duration = Date.now() - startTime;
logger.error('Restore failed', error instanceof Error ? error : undefined, {
backupId,
filePath: metadata.filePath,
durationMs: duration
});
// Log failed restore to audit trail
this.logAuditEntry({
backupId,
operation: 'restore',
agentId: options.agentId,
status: 'failure',
filePath: metadata.filePath,
backupPath: metadata.backupPath,
errorMessage: error instanceof Error ? error.message : String(error),
errorCode: error instanceof StandardError ? error.code : ErrorCode.UNKNOWN_ERROR,
durationMs: duration
});
throw error;
} finally{
await this.lockManager.releaseLock(lock.id);
}
}
/**
* Get disk usage statistics
*/ getDiskUsage() {
const stmt = this.db.prepare(`
SELECT
COUNT(*) as total_backups,
SUM(CASE WHEN deleted_at IS NULL AND expires_at > datetime('now') THEN 1 ELSE 0 END) as active_backups,
SUM(CASE WHEN deleted_at IS NULL AND expires_at <= datetime('now') THEN 1 ELSE 0 END) as expired_backups,
SUM(file_size) as total_size_bytes,
SUM(CASE WHEN is_compressed = 1 THEN file_size ELSE 0 END) as compressed_size_bytes,
AVG(CASE WHEN is_compressed = 1 THEN compression_ratio ELSE NULL END) as avg_compression_ratio,
MIN(created_at) as oldest_backup,
MAX(created_at) as newest_backup
FROM backups
WHERE deleted_at IS NULL
`);
const result = stmt.get();
const byTypeStmt = this.db.prepare(`
SELECT backup_type, COUNT(*) as count
FROM backups
WHERE deleted_at IS NULL
GROUP BY backup_type
`);
const byType = byTypeStmt.all();
const byAgentStmt = this.db.prepare(`
SELECT agent_id, COUNT(*) as count
FROM backups
WHERE deleted_at IS NULL
GROUP BY agent_id
`);
const byAgent = byAgentStmt.all();
return {
totalBackups: result.total_backups || 0,
activeBackups: result.active_backups || 0,
expiredBackups: result.expired_backups || 0,
totalSizeBytes: result.total_size_bytes || 0,
compressedSizeBytes: result.compressed_size_bytes || 0,
averageCompressionRatio: result.avg_compression_ratio || 0,
oldestBackupDate: result.oldest_backup ? new Date(result.oldest_backup) : null,
newestBackupDate: result.newest_backup ? new Date(result.newest_backup) : null,
backupsByType: byType.reduce((acc, row)=>{
acc[row.backup_type] = row.count;
return acc;
}, {}),
backupsByAgent: byAgent.reduce((acc, row)=>{
acc[row.agent_id] = row.count;
return acc;
}, {})
};
}
/**
* List backups for a file
*/ listBackups(filePath) {
const absolutePath = path.resolve(filePath);
const stmt = this.db.prepare(`
SELECT * FROM backups
WHERE file_path = ? AND deleted_at IS NULL
ORDER BY created_at DESC
`);
return stmt.all(absolutePath);
}
/**
* Delete expired backups
*/ deleteExpiredBackups() {
const expiredBackups = this.db.prepare(`
SELECT id, backup_path FROM backups
WHERE deleted_at IS NULL AND expires_at <= datetime('now')
`).all();
let deletedCount = 0;
for (const backup of expiredBackups){
try {
// Delete backup file
fs.unlinkSync(backup.backup_path);
// Mark as deleted in database
this.db.prepare(`
UPDATE backups SET deleted_at = datetime('now')
WHERE id = ?
`).run(backup.id);
deletedCount++;
} catch (error) {
logger.error('Failed to delete expired backup', error instanceof Error ? error : undefined, {
backupId: backup.id,
backupPath: backup.backup_path
});
}
}
logger.info('Expired backups deleted', {
count: deletedCount
});
return deletedCount;
}
/**
* Close database connection
*/ close() {
this.db.close();
logger.info('Backup manager closed');
}
// ============================================================================
// Private Helper Methods
// ============================================================================
initializeDatabase(dbPath) {
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, {
recursive: true
});
}
const db = new Database(dbPath);
// Run migration
const migrationPath = path.join(this.projectRoot, 'src/db/migrations/up/004-backup-metadata-schema.sql');
if (fs.existsSync(migrationPath)) {
const migration = fs.readFileSync(migrationPath, 'utf8');
db.exec(migration);
logger.info('Database migration applied', {
migrationPath
});
}
return db;
}
ensureBackupDirectory() {
if (!fs.existsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, {
recursive: true,
mode: 0o755
});
logger.info('Created backup directory', {
directory: this.backupDir
});
}
}
async ensureDirectory(dirPath) {
try {
await fsMkdir(dirPath, {
recursive: true,
mode: 0o755
});
} catch (error) {
// Ignore if directory already exists
if (error.code !== 'EEXIST') {
throw error;
}
}
}
getBackupPath(agentId, timestamp, hash) {
return path.join(this.backupDir, agentId, `${timestamp}_${hash}`, 'original');
}
calculateHash(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
/**
* Constant-time hash comparison to prevent timing attacks
* @param hash1 First hash (hex string)
* @param hash2 Second hash (hex string)
* @returns true if hashes match, false otherwise
*/ constantTimeHashCompare(hash1, hash2) {
try {
// Convert hex strings to buffers
const buffer1 = Buffer.from(hash1, 'hex');
const buffer2 = Buffer.from(hash2, 'hex');
// Length check (not timing-sensitive as length is not secret)
if (buffer1.length !== buffer2.length) {
return false;
}
// Constant-time comparison
return crypto.timingSafeEqual(buffer1, buffer2);
} catch (error) {
// Handle Buffer conversion failures
logger.error('Hash comparison failed', error instanceof Error ? error : undefined, {
hash1Length: hash1?.length,
hash2Length: hash2?.length
});
return false;
}
}
async fileExists(filePath) {
try {
await fsAccess(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
async safeUnlink(filePath) {
try {
await fsUnlink(filePath);
} catch {
// Ignore errors
}
}
insertBackup(backup, encryptionMetadata) {
const stmt = this.db.prepare(`
INSERT INTO backups (
id, agent_id, file_path, backup_path, original_hash, backup_hash,
file_size, backup_type, created_at, expires_at, metadata,
is_encrypted, encrypted_at, encryption_algorithm, encryption_iv,
encryption_auth_tag, encryption_hmac, encryption_key_version
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(backup.id, backup.agentId, backup.filePath, backup.backupPath, backup.originalHash, backup.backupHash, backup.fileSize, backup.backupType, backup.createdAt.toISOString(), backup.expiresAt.toISOString(), backup.metadata ? JSON.stringify(backup.metadata) : null, // ENCRYPTION SUPPORT (CVSS 7.2 mitigation)
encryptionMetadata ? 1 : 0, encryptionMetadata ? encryptionMetadata.encryptedAt : null, encryptionMetadata ? encryptionMetadata.algorithm : null, encryptionMetadata ? encryptionMetadata.iv : null, encryptionMetadata ? encryptionMetadata.authTag : null, encryptionMetadata ? encryptionMetadata.hmac : null, encryptionMetadata ? encryptionMetadata.keyVersion : null);
}
getBackupMetadata(backupId) {
const stmt = this.db.prepare(`
SELECT * FROM backups WHERE id = ? AND deleted_at IS NULL
`);
return stmt.get(backupId) || null;
}
getLatestBackup(filePath) {
const stmt = this.db.prepare(`
SELECT * FROM backups
WHERE file_path = ? AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 1
`);
return stmt.get(filePath) || null;
}
getBackupByTimestamp(filePath, timestamp) {
const stmt = this.db.prepare(`
SELECT * FROM backups
WHERE file_path = ? AND deleted_at IS NULL AND created_at <= ?
ORDER BY created_at DESC
LIMIT 1
`);
return stmt.get(filePath, timestamp.toISOString()) || null;
}
getBackupByHash(filePath, hash) {
const stmt = this.db.prepare(`
SELECT * FROM backups
WHERE file_path = ? AND original_hash = ? AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 1
`);
return stmt.get(filePath, hash) || null;
}
checkRateLimit(agentId) {
const stmt = this.db.prepare(`
SELECT COUNT(*) as count
FROM restore_rate_limits
WHERE agent_id = ? AND restored_at > datetime('now', '-1 hour')
`);
const result = stmt.get(agentId);
return result.count < this.rateLimitConfig.maxRestoresPerHour;
}
recordRestore(backupId, agentId, filePath) {
const stmt = this.db.prepare(`
INSERT INTO restore_rate_limits (id, backup_id, agent_id, file_path, restored_at)
VALUES (?, ?, ?, ?, datetime('now'))
`);
stmt.run(randomUUID(), backupId, agentId, filePath);
}
logAuditEntry(entry) {
const stmt = this.db.prepare(`
INSERT INTO backup_audit_log (
id, backup_id, operation, agent_id, status, file_path, backup_path,
timestamp, duration_ms, error_message, error_code, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), ?, ?, ?, ?)
`);
stmt.run(randomUUID(), entry.backupId, entry.operation, entry.agentId, entry.status, entry.filePath, entry.backupPath || null, entry.durationMs || null, entry.errorMessage || null, entry.errorCode || null, entry.metadata ? JSON.stringify(entry.metadata) : null);
}
}
/**
* Singleton instance
*/ let defaultManager = null;
/**
* Get the default backup manager instance
*/ export function getBackupManager(config) {
if (!defaultManager) {
defaultManager = new BackupManager(config);
}
return defaultManager;
}
/**
* Execute a function with automatic backup
*
* @param filePath - File to backup
* @param fn - Function to execute
* @param options - Backup options
* @returns Promise that resolves with function result
*/ export async function withBackup(filePath, fn, options) {
const manager = getBackupManager();
await manager.createBackup(filePath, options);
return fn();
}
//# sourceMappingURL=backup-manager.js.map