Skip to main content
Glama

1MCP Server

backupManager.ts10.4 kB
import crypto from 'crypto'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { getAppBackupDir, getGlobalBackupDir } from '@src/constants.js'; /** * Backup and recovery system for app configuration consolidation. * * Provides safe backup creation, metadata tracking, and rollback * capabilities for configuration operations. */ export interface BackupInfo { originalPath: string; backupPath: string; timestamp: number; checksum: string; metadata: { app: string; operation: string; version: string; serverCount: number; fileSize: number; }; } export interface BackupListItem { app: string; backupPath: string; metaPath: string; timestamp: number; operation: string; serverCount: number; age: string; } /** * Create a backup of configuration file with metadata */ export function createBackup(configPath: string, app: string, operation: string, serverCount: number = 0): BackupInfo { if (!fs.existsSync(configPath)) { throw new Error(`Configuration file does not exist: ${configPath}`); } const timestamp = Date.now(); const dateStr = new Date(timestamp).toISOString().replace(/[:.-]/g, '').slice(0, 15); // YYYYMMDDTHHMMSS // Create app-specific backup directory const appBackupDir = getAppBackupDir(app); fs.mkdirSync(appBackupDir, { recursive: true }); // Use descriptive filename in centralized location const backupFileName = `${dateStr}_${operation}.backup`; const backupPath = path.join(appBackupDir, backupFileName); const content = fs.readFileSync(configPath, 'utf8'); const checksum = crypto.createHash('sha256').update(content).digest('hex'); const stats = fs.statSync(configPath); // Create backup file fs.copyFileSync(configPath, backupPath); const backupInfo: BackupInfo = { originalPath: configPath, backupPath, timestamp, checksum, metadata: { app, operation, version: process.env.npm_package_version || 'unknown', serverCount, fileSize: stats.size, }, }; // Store backup metadata const metaPath = `${backupPath}.meta`; fs.writeFileSync(metaPath, JSON.stringify(backupInfo, null, 2)); return backupInfo; } /** * Rollback from backup file */ export async function rollbackFromBackup(backup: BackupInfo): Promise<void> { if (!fs.existsSync(backup.backupPath)) { throw new Error(`Backup file not found: ${backup.backupPath}`); } // Verify backup integrity const backupContent = fs.readFileSync(backup.backupPath, 'utf8'); const currentChecksum = crypto.createHash('sha256').update(backupContent).digest('hex'); if (currentChecksum !== backup.checksum) { throw new Error('Backup file integrity check failed - checksum mismatch'); } // Restore original file fs.copyFileSync(backup.backupPath, backup.originalPath); } /** * Rollback from backup file path */ export async function rollbackFromBackupPath(backupPath: string): Promise<void> { const metaPath = `${backupPath}.meta`; if (!fs.existsSync(metaPath)) { throw new Error(`Backup metadata not found: ${metaPath}`); } const metaContent = fs.readFileSync(metaPath, 'utf8'); const backupInfo: BackupInfo = JSON.parse(metaContent); await rollbackFromBackup(backupInfo); } /** * List all available backups for a specific app */ export function listAppBackups(app?: string): BackupListItem[] { const backups: BackupListItem[] = []; // First, check the centralized backup directory (most efficient) const centralBackupDir = getGlobalBackupDir(); if (fs.existsSync(centralBackupDir)) { if (app) { // Search specific app directory const appBackupDir = getAppBackupDir(app); scanBackupsInDirectory(appBackupDir, backups, app); } else { // Search all app directories try { const appDirs = fs.readdirSync(centralBackupDir); for (const appDir of appDirs) { const appBackupPath = path.join(centralBackupDir, appDir); if (fs.statSync(appBackupPath).isDirectory()) { scanBackupsInDirectory(appBackupPath, backups); } } } catch (_error) { // Skip if can't read central directory } } } // Then check legacy locations for existing backups const legacyPaths = getBackupSearchPaths().slice(1); // Skip central directory (already checked) for (const searchPath of legacyPaths) { scanBackupsInDirectory(searchPath, backups, app); } // Sort by timestamp (newest first) backups.sort((a, b) => b.timestamp - a.timestamp); return backups; } /** * Scan a directory for backup files */ function scanBackupsInDirectory(dirPath: string, backups: BackupListItem[], filterApp?: string): void { if (!fs.existsSync(dirPath)) { return; } try { const files = fs.readdirSync(dirPath); for (const file of files) { if (file.endsWith('.backup.meta')) { const metaPath = path.join(dirPath, file); try { const metaContent = fs.readFileSync(metaPath, 'utf8'); const backupInfo: BackupInfo = JSON.parse(metaContent); // Filter by app if specified if (filterApp && backupInfo.metadata.app !== filterApp) { continue; } const backupPath = backupInfo.backupPath; // Check if backup file still exists if (!fs.existsSync(backupPath)) { continue; } backups.push({ app: backupInfo.metadata.app, backupPath, metaPath, timestamp: backupInfo.timestamp, operation: backupInfo.metadata.operation, serverCount: backupInfo.metadata.serverCount, age: formatAge(backupInfo.timestamp), }); } catch (_error) { // Skip invalid metadata files continue; } } } } catch (_error) { // Skip directories we can't read return; } } /** * Get search paths for backup files */ function getBackupSearchPaths(): string[] { const paths = [ // Primary location: centralized backup directory getGlobalBackupDir(), ]; // Legacy fallback locations for existing backups const homeDir = os.homedir(); // Add current directory as fallback paths.push(process.cwd()); // Add common app config directories as fallback if (process.platform === 'darwin') { paths.push( path.join(homeDir, 'Library/Application Support/Claude'), path.join(homeDir, '.cursor'), path.join(homeDir, '.vscode'), ); } else if (process.platform === 'win32') { const appData = process.env.APPDATA || ''; if (appData) { paths.push(path.join(appData, 'Claude'), path.join(homeDir, '.cursor'), path.join(homeDir, '.vscode')); } } else { paths.push( path.join(homeDir, '.config/claude'), path.join(homeDir, '.cursor'), path.join(homeDir, '.vscode'), path.join(homeDir, '.config/cline'), ); } return paths.filter((p) => typeof p === 'string' && p.length > 0); } /** * Find backup by metadata file path */ export function findBackupByMetaPath(metaPath: string): BackupInfo | null { try { if (!fs.existsSync(metaPath)) { return null; } const metaContent = fs.readFileSync(metaPath, 'utf8'); const backupInfo: BackupInfo = JSON.parse(metaContent); // Verify backup file exists if (!fs.existsSync(backupInfo.backupPath)) { return null; } return backupInfo; } catch (_error) { return null; } } /** * Clean up old backups (older than specified days) */ export function cleanupOldBackups(maxAgeDays: number = 30): number { const backups = listAppBackups(); const cutoffTime = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000; let cleanedCount = 0; for (const backup of backups) { if (backup.timestamp < cutoffTime) { try { // Remove backup file if (fs.existsSync(backup.backupPath)) { fs.unlinkSync(backup.backupPath); } // Remove metadata file if (fs.existsSync(backup.metaPath)) { fs.unlinkSync(backup.metaPath); } cleanedCount++; } catch (_error) { // Skip files we can't delete continue; } } } return cleanedCount; } /** * Verify backup integrity */ export function verifyBackupIntegrity(backupInfo: BackupInfo): boolean { try { if (!fs.existsSync(backupInfo.backupPath)) { return false; } const content = fs.readFileSync(backupInfo.backupPath, 'utf8'); const checksum = crypto.createHash('sha256').update(content).digest('hex'); return checksum === backupInfo.checksum; } catch (_error) { return false; } } /** * Format backup age for display */ function formatAge(timestamp: number): string { const now = Date.now(); const diffMs = now - timestamp; const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMinutes < 1) { return 'just now'; } else if (diffMinutes < 60) { return `${diffMinutes}m ago`; } else if (diffHours < 24) { return `${diffHours}h ago`; } else if (diffDays < 7) { return `${diffDays}d ago`; } else { const date = new Date(timestamp); return date.toLocaleDateString(); } } /** * File locking mechanism for atomic operations */ export async function withFileLock<T>(filePath: string, operation: () => Promise<T>): Promise<T> { const lockPath = `${filePath}.lock`; // Simple file-based locking mechanism let lockAcquired = false; const maxRetries = 10; const retryDelay = 100; for (let i = 0; i < maxRetries; i++) { try { fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' }); lockAcquired = true; break; } catch (error: any) { if (error.code === 'EEXIST') { // Lock file exists, wait and retry await new Promise((resolve) => setTimeout(resolve, retryDelay)); continue; } throw error; } } if (!lockAcquired) { throw new Error(`Failed to acquire lock for ${filePath} after ${maxRetries} retries`); } try { const result = await operation(); return result; } finally { // Clean up lock file try { fs.unlinkSync(lockPath); } catch (_error) { // Ignore cleanup errors } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1mcp-app/agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server