Skip to main content
Glama
danielsimonjr

Enhanced Knowledge Graph Memory Server

BackupManager.ts10.4 kB
/** * Backup Manager * * Manages backup and restore operations for the knowledge graph. * Provides point-in-time recovery and data protection. * * @module features/BackupManager */ import { promises as fs } from 'fs'; import { dirname, join } from 'path'; import type { GraphStorage } from '../core/GraphStorage.js'; import { FileOperationError } from '../utils/errors.js'; /** * Metadata stored with each backup. */ export interface BackupMetadata { /** Timestamp when backup was created (ISO 8601) */ timestamp: string; /** Number of entities in the backup */ entityCount: number; /** Number of relations in the backup */ relationCount: number; /** File size in bytes */ fileSize: number; /** Optional description/reason for backup */ description?: string; } /** * Information about a backup file. */ export interface BackupInfo { /** Backup file name */ fileName: string; /** Full path to backup file */ filePath: string; /** Backup metadata */ metadata: BackupMetadata; } /** * Manages backup and restore operations for the knowledge graph. * * Backup files are stored in a `.backups` directory next to the main graph file. * Each backup includes the graph data and metadata about the backup. * * @example * ```typescript * const storage = new GraphStorage('/data/memory.jsonl'); * const backup = new BackupManager(storage); * * // Create a backup * const backupPath = await backup.createBackup('Before compression'); * * // List available backups * const backups = await backup.listBackups(); * * // Restore from backup * await backup.restoreFromBackup(backupPath); * * // Clean old backups (keep last 10) * await backup.cleanOldBackups(10); * ``` */ export class BackupManager { private backupDir: string; constructor(private storage: GraphStorage) { const filePath = this.storage.getFilePath(); const dir = dirname(filePath); this.backupDir = join(dir, '.backups'); } /** * Ensure backup directory exists. * * @private */ private async ensureBackupDir(): Promise<void> { try { await fs.mkdir(this.backupDir, { recursive: true }); } catch (error) { throw new FileOperationError('create backup directory', this.backupDir, error as Error); } } /** * Generate backup file name with timestamp. * * Format: backup_YYYY-MM-DD_HH-MM-SS-mmm.jsonl * * @private */ private generateBackupFileName(): string { const now = new Date(); const timestamp = now.toISOString() .replace(/:/g, '-') .replace(/\./g, '-') .replace('T', '_') .replace('Z', ''); return `backup_${timestamp}.jsonl`; } /** * Create a backup of the current knowledge graph. * * Backup includes: * - Complete graph data (entities and relations) * - Metadata (timestamp, counts, file size, description) * * @param description - Optional description for this backup * @returns Promise resolving to the backup file path * @throws {FileOperationError} If backup creation fails * * @example * ```typescript * // Create backup with description * const backupPath = await manager.createBackup('Before merging duplicates'); * * // Create backup without description * const backupPath = await manager.createBackup(); * ``` */ async createBackup(description?: string): Promise<string> { await this.ensureBackupDir(); // Load current graph const graph = await this.storage.loadGraph(); const fileName = this.generateBackupFileName(); const backupPath = join(this.backupDir, fileName); try { // Read the current file content to preserve exact formatting const originalPath = this.storage.getFilePath(); let fileContent: string; try { fileContent = await fs.readFile(originalPath, 'utf-8'); } catch (error) { // File doesn't exist yet - generate content from graph const lines = [ ...graph.entities.map(e => JSON.stringify({ type: 'entity', ...e })), ...graph.relations.map(r => JSON.stringify({ type: 'relation', ...r })), ]; fileContent = lines.join('\n'); } // Write backup file await fs.writeFile(backupPath, fileContent); // Get file stats const stats = await fs.stat(backupPath); // Create metadata const metadata: BackupMetadata = { timestamp: new Date().toISOString(), entityCount: graph.entities.length, relationCount: graph.relations.length, fileSize: stats.size, description, }; // Write metadata file const metadataPath = `${backupPath}.meta.json`; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); return backupPath; } catch (error) { throw new FileOperationError('create backup', backupPath, error as Error); } } /** * List all available backups, sorted by timestamp (newest first). * * @returns Promise resolving to array of backup information * @throws {FileOperationError} If listing fails * * @example * ```typescript * const backups = await manager.listBackups(); * console.log(`Found ${backups.length} backups`); * * for (const backup of backups) { * console.log(`${backup.fileName}: ${backup.metadata.entityCount} entities`); * } * ``` */ async listBackups(): Promise<BackupInfo[]> { try { // Check if backup directory exists try { await fs.access(this.backupDir); } catch { // Directory doesn't exist - no backups return []; } const files = await fs.readdir(this.backupDir); const backupFiles = files.filter(f => f.startsWith('backup_') && f.endsWith('.jsonl')); const backups: BackupInfo[] = []; for (const fileName of backupFiles) { const filePath = join(this.backupDir, fileName); const metadataPath = `${filePath}.meta.json`; try { // Read metadata if it exists const metadataContent = await fs.readFile(metadataPath, 'utf-8'); const metadata: BackupMetadata = JSON.parse(metadataContent); backups.push({ fileName, filePath, metadata, }); } catch { // Metadata file doesn't exist or is corrupt - skip this backup continue; } } // Sort by timestamp (newest first) backups.sort((a, b) => new Date(b.metadata.timestamp).getTime() - new Date(a.metadata.timestamp).getTime() ); return backups; } catch (error) { throw new FileOperationError('list backups', this.backupDir, error as Error); } } /** * Restore the knowledge graph from a backup file. * * CAUTION: This operation overwrites the current graph with backup data. * Consider creating a backup before restoring. * * @param backupPath - Path to the backup file to restore from * @returns Promise that resolves when restore is complete * @throws {FileOperationError} If restore fails * * @example * ```typescript * // List backups and restore the most recent * const backups = await manager.listBackups(); * if (backups.length > 0) { * await manager.restoreFromBackup(backups[0].filePath); * } * * // Restore specific backup by path * await manager.restoreFromBackup('/data/.backups/backup_2024-01-15_10-30-00.jsonl'); * ``` */ async restoreFromBackup(backupPath: string): Promise<void> { try { // Verify backup file exists await fs.access(backupPath); // Read backup content const backupContent = await fs.readFile(backupPath, 'utf-8'); // Write to main graph file const mainPath = this.storage.getFilePath(); await fs.writeFile(mainPath, backupContent); // Clear storage cache to force reload this.storage.clearCache(); } catch (error) { throw new FileOperationError('restore from backup', backupPath, error as Error); } } /** * Delete a specific backup file. * * Also deletes the associated metadata file. * * @param backupPath - Path to the backup file to delete * @returns Promise that resolves when deletion is complete * @throws {FileOperationError} If deletion fails * * @example * ```typescript * const backups = await manager.listBackups(); * // Delete oldest backup * if (backups.length > 0) { * await manager.deleteBackup(backups[backups.length - 1].filePath); * } * ``` */ async deleteBackup(backupPath: string): Promise<void> { try { // Delete backup file await fs.unlink(backupPath); // Delete metadata file (ignore errors if it doesn't exist) try { await fs.unlink(`${backupPath}.meta.json`); } catch { // Metadata file doesn't exist - that's ok } } catch (error) { throw new FileOperationError('delete backup', backupPath, error as Error); } } /** * Clean old backups, keeping only the most recent N backups. * * Backups are sorted by timestamp, and older backups are deleted. * * @param keepCount - Number of recent backups to keep (default: 10) * @returns Promise resolving to number of backups deleted * @throws {FileOperationError} If cleanup fails * * @example * ```typescript * // Keep only the 5 most recent backups * const deleted = await manager.cleanOldBackups(5); * console.log(`Deleted ${deleted} old backups`); * * // Keep default 10 most recent backups * await manager.cleanOldBackups(); * ``` */ async cleanOldBackups(keepCount: number = 10): Promise<number> { const backups = await this.listBackups(); // If we have fewer backups than keepCount, nothing to delete if (backups.length <= keepCount) { return 0; } // Delete backups beyond keepCount const backupsToDelete = backups.slice(keepCount); let deletedCount = 0; for (const backup of backupsToDelete) { try { await this.deleteBackup(backup.filePath); deletedCount++; } catch { // Continue deleting other backups even if one fails continue; } } return deletedCount; } /** * Get the path to the backup directory. * * @returns The backup directory path */ getBackupDir(): string { return this.backupDir; } }

Latest Blog Posts

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/danielsimonjr/memory-mcp'

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