supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
667 lines โข 25.9 kB
JavaScript
;
/**
* Configuration Backup Manager System
* Phase 4, Checkpoint D2 - Advanced backup management with versioning and restoration
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigurationBackupManager = void 0;
const logger_1 = require("../core/utils/logger");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const zlib = __importStar(require("zlib"));
class ConfigurationBackupManager {
constructor(backupDirectory) {
this.backups = new Map();
this.backupSets = new Map();
this.policies = new Map();
this.backupDirectory = backupDirectory || path.join(process.cwd(), '.supa-seed', 'backups');
this.metadataFile = path.join(this.backupDirectory, 'backup-metadata.json');
this.initializeDirectory();
this.loadMetadata();
this.initializeDefaultPolicies();
}
/**
* Create a backup of a single file
*/
async createBackup(sourceFile, reason = 'manual', tags = [], relatedUpdates = []) {
logger_1.Logger.debug(`๐ฆ Creating backup for: ${sourceFile}`);
if (!fs.existsSync(sourceFile)) {
throw new Error(`Source file does not exist: ${sourceFile}`);
}
const timestamp = new Date();
const backupId = this.generateBackupId(sourceFile, timestamp);
const stats = fs.statSync(sourceFile);
// Generate backup filename
const sourceBasename = path.basename(sourceFile);
const backupFilename = `${backupId}-${sourceBasename}`;
const backupPath = path.join(this.backupDirectory, 'files', backupFilename);
// Ensure backup directory exists
const backupDir = path.dirname(backupPath);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
// Read source file
const sourceContent = fs.readFileSync(sourceFile);
const checksum = crypto.createHash('sha256').update(sourceContent).digest('hex');
// Apply compression if enabled
let finalContent = sourceContent;
let compressed = false;
const compressionPolicy = this.shouldCompress(sourceFile, sourceContent.length);
if (compressionPolicy) {
finalContent = zlib.gzipSync(sourceContent);
compressed = true;
logger_1.Logger.debug(`Compressed backup: ${sourceContent.length} -> ${finalContent.length} bytes`);
}
// Write backup file
fs.writeFileSync(backupPath, finalContent);
// Create metadata
const metadata = {
id: backupId,
timestamp,
sourceFile: path.resolve(sourceFile),
backupPath,
checksum,
size: stats.size,
compressed,
tags,
reason,
relatedUpdates,
metadata: {
environment: process.env.NODE_ENV || 'development',
notes: `Backup created for ${reason} operation`
}
};
// Store metadata
this.backups.set(backupId, metadata);
await this.saveMetadata();
logger_1.Logger.info(`๐ฆ Backup created: ${backupId} (${this.formatSize(stats.size)})`);
return metadata;
}
/**
* Create a backup set for multiple files
*/
async createBackupSet(files, name, description, relatedChanges = []) {
logger_1.Logger.info(`๐ฆ Creating backup set: ${name} (${files.length} files)`);
const backupSetId = `backupset-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const timestamp = new Date();
const backups = [];
let totalSize = 0;
// Create individual backups
for (const file of files) {
if (fs.existsSync(file)) {
try {
const backup = await this.createBackup(file, 'pre_update', [`backupset:${backupSetId}`], relatedChanges);
backups.push(backup);
totalSize += backup.size;
}
catch (error) {
logger_1.Logger.warn(`Failed to backup file ${file}: ${error.message}`);
}
}
}
const backupSet = {
id: backupSetId,
name,
description,
timestamp,
backups,
status: backups.length === files.length ? 'complete' : 'partial',
totalSize,
relatedChanges
};
this.backupSets.set(backupSetId, backupSet);
await this.saveMetadata();
logger_1.Logger.info(`๐ฆ Backup set created: ${backupSetId} (${backups.length}/${files.length} files, ${this.formatSize(totalSize)})`);
return backupSet;
}
/**
* Restore a single backup
*/
async restoreBackup(backupId, targetPath, overwrite = false) {
const backup = this.backups.get(backupId);
if (!backup) {
return {
success: false,
restoredFiles: [],
errors: [{
code: 'BACKUP_NOT_FOUND',
message: `Backup not found: ${backupId}`,
filePath: '',
severity: 'error'
}],
warnings: [],
metadata: {
backupSetId: '',
restoreTime: new Date(),
totalFiles: 0,
totalSize: 0
}
};
}
return this.performRestore([backup], targetPath, overwrite);
}
/**
* Restore a backup set
*/
async restoreBackupSet(backupSetId, targetDirectory, overwrite = false) {
const backupSet = this.backupSets.get(backupSetId);
if (!backupSet) {
return {
success: false,
restoredFiles: [],
errors: [{
code: 'BACKUP_SET_NOT_FOUND',
message: `Backup set not found: ${backupSetId}`,
filePath: '',
severity: 'error'
}],
warnings: [],
metadata: {
backupSetId,
restoreTime: new Date(),
totalFiles: 0,
totalSize: 0
}
};
}
return this.performRestore(backupSet.backups, targetDirectory, overwrite, backupSetId);
}
/**
* List all backups
*/
listBackups(filter) {
let results = Array.from(this.backups.values());
if (filter) {
if (filter.sourceFile) {
results = results.filter(b => b.sourceFile.includes(filter.sourceFile));
}
if (filter.reason) {
results = results.filter(b => b.reason === filter.reason);
}
if (filter.tags && filter.tags.length > 0) {
results = results.filter(b => filter.tags.some(tag => b.tags.includes(tag)));
}
if (filter.afterDate) {
results = results.filter(b => b.timestamp >= filter.afterDate);
}
if (filter.beforeDate) {
results = results.filter(b => b.timestamp <= filter.beforeDate);
}
}
return results.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
/**
* List all backup sets
*/
listBackupSets() {
return Array.from(this.backupSets.values())
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
/**
* Verify backup integrity
*/
async verifyBackups(backupIds) {
logger_1.Logger.info('๐ Verifying backup integrity...');
const startTime = Date.now();
const toVerify = backupIds
? backupIds.map(id => this.backups.get(id)).filter(Boolean)
: Array.from(this.backups.values());
const corruptedBackups = [];
const missingFiles = [];
const checksumMismatches = [];
for (const backup of toVerify) {
try {
// Check if backup file exists
if (!fs.existsSync(backup.backupPath)) {
missingFiles.push(backup.id);
continue;
}
// Read backup file
let backupContent = fs.readFileSync(backup.backupPath);
// Decompress if needed
if (backup.compressed) {
try {
backupContent = zlib.gunzipSync(backupContent);
}
catch (error) {
corruptedBackups.push(backup.id);
continue;
}
}
// Verify checksum
const actualChecksum = crypto.createHash('sha256').update(backupContent).digest('hex');
if (actualChecksum !== backup.checksum) {
checksumMismatches.push(backup.id);
}
}
catch (error) {
logger_1.Logger.warn(`Verification failed for backup ${backup.id}: ${error.message}`);
corruptedBackups.push(backup.id);
}
}
const verificationTime = Date.now() - startTime;
const isValid = corruptedBackups.length === 0 && missingFiles.length === 0 && checksumMismatches.length === 0;
logger_1.Logger.info(`๐ Verification complete: ${toVerify.length} backups verified in ${verificationTime}ms`);
if (!isValid) {
logger_1.Logger.warn(`โ ๏ธ Issues found: ${corruptedBackups.length} corrupted, ${missingFiles.length} missing, ${checksumMismatches.length} checksum mismatches`);
}
return {
isValid,
corruptedBackups,
missingFiles,
checksumMismatches,
totalVerified: toVerify.length,
verificationTime
};
}
/**
* Apply retention policies to clean up old backups
*/
async applyRetentionPolicies() {
logger_1.Logger.info('๐งน Applying retention policies...');
const deletedBackups = [];
const errors = [];
let totalSpaceFreed = 0;
// Get all backups sorted by date (newest first)
const allBackups = this.listBackups();
// Apply default retention policy if no specific policies are defined
if (this.policies.size === 0) {
const toDelete = this.applyDefaultRetention(allBackups);
for (const backup of toDelete) {
try {
await this.deleteBackup(backup.id);
deletedBackups.push(backup.id);
totalSpaceFreed += backup.size;
}
catch (error) {
errors.push(`Failed to delete backup ${backup.id}: ${error.message}`);
}
}
}
else {
// Apply custom policies
for (const policy of this.policies.values()) {
const policyBackups = this.getBackupsForPolicy(policy, allBackups);
const toDelete = this.applyRetentionPolicy(policy.retention, policyBackups);
for (const backup of toDelete) {
try {
await this.deleteBackup(backup.id);
deletedBackups.push(backup.id);
totalSpaceFreed += backup.size;
}
catch (error) {
errors.push(`Failed to delete backup ${backup.id}: ${error.message}`);
}
}
}
}
await this.saveMetadata();
logger_1.Logger.info(`๐งน Retention cleanup complete: deleted ${deletedBackups.length} backups, freed ${this.formatSize(totalSpaceFreed)}`);
return {
deletedBackups,
totalSpaceFreed,
errors
};
}
/**
* Delete a backup
*/
async deleteBackup(backupId) {
const backup = this.backups.get(backupId);
if (!backup) {
return false;
}
try {
// Delete backup file
if (fs.existsSync(backup.backupPath)) {
fs.unlinkSync(backup.backupPath);
}
// Remove from metadata
this.backups.delete(backupId);
// Remove from backup sets
for (const backupSet of this.backupSets.values()) {
backupSet.backups = backupSet.backups.filter(b => b.id !== backupId);
if (backupSet.backups.length === 0) {
this.backupSets.delete(backupSet.id);
}
}
await this.saveMetadata();
logger_1.Logger.debug(`Deleted backup: ${backupId}`);
return true;
}
catch (error) {
logger_1.Logger.error(`Failed to delete backup ${backupId}: ${error.message}`);
return false;
}
}
/**
* Get backup statistics
*/
getBackupStatistics() {
const backups = Array.from(this.backups.values());
const totalSize = backups.reduce((sum, b) => sum + b.size, 0);
const compressedSize = backups
.filter(b => b.compressed)
.reduce((sum, b) => sum + fs.statSync(b.backupPath).size, 0);
const uncompressedSize = backups
.filter(b => !b.compressed)
.reduce((sum, b) => sum + fs.statSync(b.backupPath).size, 0);
const backupsByReason = {};
backups.forEach(backup => {
backupsByReason[backup.reason] = (backupsByReason[backup.reason] || 0) + 1;
});
return {
totalBackups: backups.length,
totalBackupSets: this.backupSets.size,
totalSize,
oldestBackup: backups.length > 0 ? new Date(Math.min(...backups.map(b => b.timestamp.getTime()))) : undefined,
newestBackup: backups.length > 0 ? new Date(Math.max(...backups.map(b => b.timestamp.getTime()))) : undefined,
backupsByReason,
compressionRatio: totalSize > 0 ? (compressedSize + uncompressedSize) / totalSize : 1
};
}
/**
* Add backup policy
*/
addBackupPolicy(policy) {
this.policies.set(policy.id, policy);
logger_1.Logger.debug(`Added backup policy: ${policy.name}`);
}
/**
* Remove backup policy
*/
removeBackupPolicy(policyId) {
return this.policies.delete(policyId);
}
/**
* Private: Perform the actual restore operation
*/
async performRestore(backups, targetPath, overwrite = false, backupSetId) {
logger_1.Logger.info(`๐ Restoring ${backups.length} backups...`);
const errors = [];
const warnings = [];
const restoredFiles = [];
let totalSize = 0;
for (const backup of backups) {
try {
// Determine target path
const restorePath = targetPath || backup.sourceFile;
// Check if target exists and handle overwrite
if (fs.existsSync(restorePath) && !overwrite) {
warnings.push({
code: 'FILE_EXISTS',
message: `Target file exists and overwrite is disabled: ${restorePath}`,
filePath: restorePath,
impact: 'medium'
});
continue;
}
// Read backup file
if (!fs.existsSync(backup.backupPath)) {
errors.push({
code: 'BACKUP_FILE_MISSING',
message: `Backup file not found: ${backup.backupPath}`,
filePath: backup.sourceFile,
severity: 'error'
});
continue;
}
let backupContent = fs.readFileSync(backup.backupPath);
// Decompress if needed
if (backup.compressed) {
try {
backupContent = zlib.gunzipSync(backupContent);
}
catch (error) {
errors.push({
code: 'DECOMPRESSION_FAILED',
message: `Failed to decompress backup: ${error.message}`,
filePath: backup.sourceFile,
severity: 'error'
});
continue;
}
}
// Verify checksum
const actualChecksum = crypto.createHash('sha256').update(backupContent).digest('hex');
if (actualChecksum !== backup.checksum) {
errors.push({
code: 'CHECKSUM_MISMATCH',
message: `Backup checksum mismatch: ${backup.id}`,
filePath: backup.sourceFile,
severity: 'critical'
});
continue;
}
// Ensure target directory exists
const targetDir = path.dirname(restorePath);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// Write restored file
fs.writeFileSync(restorePath, backupContent);
restoredFiles.push(restorePath);
totalSize += backup.size;
logger_1.Logger.debug(`Restored: ${backup.sourceFile} -> ${restorePath}`);
}
catch (error) {
errors.push({
code: 'RESTORE_FAILED',
message: `Failed to restore backup: ${error.message}`,
filePath: backup.sourceFile,
severity: 'error'
});
}
}
const success = errors.filter(e => e.severity === 'critical').length === 0;
logger_1.Logger.info(`๐ Restore complete: ${restoredFiles.length}/${backups.length} files restored (${this.formatSize(totalSize)})`);
return {
success,
restoredFiles,
errors,
warnings,
metadata: {
backupSetId: backupSetId || '',
restoreTime: new Date(),
totalFiles: restoredFiles.length,
totalSize
}
};
}
/**
* Private: Initialize backup directory structure
*/
initializeDirectory() {
const dirs = [
this.backupDirectory,
path.join(this.backupDirectory, 'files'),
path.join(this.backupDirectory, 'metadata')
];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
}
/**
* Private: Load backup metadata from disk
*/
loadMetadata() {
if (fs.existsSync(this.metadataFile)) {
try {
const data = JSON.parse(fs.readFileSync(this.metadataFile, 'utf8'));
// Load backups
if (data.backups) {
for (const [id, backup] of Object.entries(data.backups)) {
this.backups.set(id, {
...backup,
timestamp: new Date(backup.timestamp)
});
}
}
// Load backup sets
if (data.backupSets) {
for (const [id, backupSet] of Object.entries(data.backupSets)) {
this.backupSets.set(id, {
...backupSet,
timestamp: new Date(backupSet.timestamp),
backups: backupSet.backups.map((b) => ({
...b,
timestamp: new Date(b.timestamp)
}))
});
}
}
logger_1.Logger.debug(`Loaded metadata: ${this.backups.size} backups, ${this.backupSets.size} backup sets`);
}
catch (error) {
logger_1.Logger.warn(`Failed to load backup metadata: ${error.message}`);
}
}
}
/**
* Private: Save backup metadata to disk
*/
async saveMetadata() {
try {
const data = {
backups: Object.fromEntries(this.backups),
backupSets: Object.fromEntries(this.backupSets),
lastUpdated: new Date().toISOString()
};
fs.writeFileSync(this.metadataFile, JSON.stringify(data, null, 2));
}
catch (error) {
logger_1.Logger.error(`Failed to save backup metadata: ${error.message}`);
}
}
/**
* Private: Initialize default backup policies
*/
initializeDefaultPolicies() {
this.addBackupPolicy({
id: 'default-retention',
name: 'Default Retention Policy',
description: 'Keep last 10 backups, delete older than 30 days',
triggers: [
{ type: 'before_update', parameters: {} }
],
retention: {
maxCount: 10,
maxAge: 30
},
compression: true,
encryption: false,
verification: true,
excludePatterns: ['*.tmp', '*.log']
});
}
/**
* Private: Generate unique backup ID
*/
generateBackupId(sourceFile, timestamp) {
const basename = path.basename(sourceFile, path.extname(sourceFile));
const hash = crypto.createHash('md5').update(sourceFile).digest('hex').substr(0, 8);
const timeStr = timestamp.toISOString().replace(/[:.]/g, '-').substr(0, 19);
return `${timeStr}-${basename}-${hash}`;
}
/**
* Private: Determine if file should be compressed
*/
shouldCompress(filePath, size) {
// Compress files larger than 1KB
if (size < 1024)
return false;
// Already compressed formats
const compressedExts = ['.gz', '.zip', '.tar', '.bz2', '.xz'];
if (compressedExts.some(ext => filePath.endsWith(ext))) {
return false;
}
return true;
}
/**
* Private: Apply default retention policy
*/
applyDefaultRetention(backups) {
const maxCount = 10;
const maxAgeDays = 30;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
return backups
.filter((backup, index) => index >= maxCount || backup.timestamp < cutoffDate);
}
/**
* Private: Apply retention policy to backups
*/
applyRetentionPolicy(policy, backups) {
const toDelete = [];
// Apply max count policy
if (policy.maxCount && backups.length > policy.maxCount) {
toDelete.push(...backups.slice(policy.maxCount));
}
// Apply max age policy
if (policy.maxAge) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - policy.maxAge);
toDelete.push(...backups.filter(b => b.timestamp < cutoffDate));
}
return [...new Set(toDelete)]; // Remove duplicates
}
/**
* Private: Get backups that match a policy
*/
getBackupsForPolicy(policy, allBackups) {
// For now, return all backups - in a real implementation,
// you'd filter based on policy triggers and exclude patterns
return allBackups;
}
/**
* Private: Format file size for display
*/
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${Math.round(size * 100) / 100} ${units[unitIndex]}`;
}
}
exports.ConfigurationBackupManager = ConfigurationBackupManager;
//# sourceMappingURL=backup-manager.js.map