Skip to main content
Glama
file-operations-service.ts10.7 kB
// File Operations Service - Export, Import, Backup, Restore import * as fs from 'fs'; import * as path from 'path'; import { logger } from '../utils/logger.js'; import { ErrorHandler, MCPError, ErrorCategory } from '../utils/error-handler.js'; import { InputValidator, SecureFileOperations } from '../utils/security.js'; export interface ExportOptions { includeMetadata?: boolean; compress?: boolean; prettify?: boolean; } export interface ImportOptions { validateStructure?: boolean; backupBefore?: boolean; overwrite?: boolean; } export interface BackupMetadata { postId: number; timestamp: string; checksum: string; version: string; fileSize: number; } export class FileOperationsService { private exportsDir: string; private importsDir: string; private backupsDir: string; constructor(baseDir: string = './data') { this.exportsDir = path.join(baseDir, 'exports'); this.importsDir = path.join(baseDir, 'imports'); this.backupsDir = path.join(baseDir, 'backups'); // Ensure directories exist this.ensureDirectories(); } private ensureDirectories(): void { [this.exportsDir, this.importsDir, this.backupsDir].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); logger.debug(`Created directory: ${dir}`); } }); } /** * Export Elementor data to file */ async exportToFile( postId: number, data: any, options: ExportOptions = {} ): Promise<string> { return ErrorHandler.wrapAsync(async () => { // Validate post ID const validPostId = InputValidator.validatePostId(postId); logger.info(`Exporting Elementor data for post ${validPostId}`); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileName = `page-${validPostId}-${timestamp}.json`; const filePath = path.join(this.exportsDir, fileName); // Validate file path is safe SecureFileOperations.isPathSafe(filePath, this.exportsDir); const exportData: any = { postId, exportedAt: new Date().toISOString(), data }; if (options.includeMetadata) { exportData.metadata = { version: '1.0.0', tool: 'ultimate-elementor-mcp', checksum: this.generateChecksum(JSON.stringify(data)) }; } const jsonString = options.prettify ? JSON.stringify(exportData, null, 2) : JSON.stringify(exportData); fs.writeFileSync(filePath, jsonString, 'utf-8'); logger.info(`Data exported successfully to ${filePath}`); return filePath; }, 'FileOperationsService.exportToFile'); } /** * Import Elementor data from file */ async importFromFile( filePath: string, options: ImportOptions = {} ): Promise<any> { return ErrorHandler.wrapAsync(async () => { // Sanitize and validate file path const sanitizedPath = InputValidator.sanitizeFilePath(filePath); SecureFileOperations.validateFileExtension(sanitizedPath); logger.info(`Importing data from ${sanitizedPath}`); // Validate file exists if (!fs.existsSync(sanitizedPath)) { throw new MCPError( `File not found: ${sanitizedPath}`, ErrorCategory.FILE_OPERATION, 'FILE_NOT_FOUND' ); } // Validate file size const stats = fs.statSync(sanitizedPath); SecureFileOperations.validateFileSize(stats.size); // Read file const fileContent = fs.readFileSync(filePath, 'utf-8'); // Parse JSON let importData: any; try { importData = JSON.parse(fileContent); } catch (error) { throw new MCPError( 'Invalid JSON file', ErrorCategory.FILE_OPERATION, 'INVALID_JSON', { error } ); } // Validate structure if (options.validateStructure) { this.validateImportData(importData); } logger.info('Data imported successfully', { postId: importData.postId, hasMetadata: !!importData.metadata }); return importData; }, 'FileOperationsService.importFromFile'); } /** * Create backup of Elementor data */ async createBackup( postId: number, data: any, metadata?: Partial<BackupMetadata> ): Promise<string> { return ErrorHandler.wrapAsync(async () => { logger.info(`Creating backup for post ${postId}`); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileName = `backup-page-${postId}-${timestamp}.json`; const filePath = path.join(this.backupsDir, fileName); const backupData = { postId, timestamp: new Date().toISOString(), checksum: this.generateChecksum(JSON.stringify(data)), version: metadata?.version || '1.0.0', data }; fs.writeFileSync(filePath, JSON.stringify(backupData, null, 2), 'utf-8'); const stats = fs.statSync(filePath); logger.info(`Backup created successfully`, { filePath, size: stats.size }); return filePath; }, 'FileOperationsService.createBackup'); } /** * Restore from backup */ async restoreFromBackup(filePath: string): Promise<any> { return ErrorHandler.wrapAsync(async () => { logger.info(`Restoring from backup: ${filePath}`); if (!fs.existsSync(filePath)) { throw new MCPError( `Backup file not found: ${filePath}`, ErrorCategory.FILE_OPERATION, 'FILE_NOT_FOUND' ); } const fileContent = fs.readFileSync(filePath, 'utf-8'); const backupData = JSON.parse(fileContent); // Verify checksum const currentChecksum = this.generateChecksum(JSON.stringify(backupData.data)); if (currentChecksum !== backupData.checksum) { logger.warn('Backup checksum mismatch - file may be corrupted'); } logger.info('Backup restored successfully', { postId: backupData.postId }); return backupData; }, 'FileOperationsService.restoreFromBackup'); } /** * List available exports */ async listExports(): Promise<any[]> { return ErrorHandler.wrapAsync(async () => { if (!fs.existsSync(this.exportsDir)) { return []; } const files = fs.readdirSync(this.exportsDir); const exports = files .filter(f => f.endsWith('.json')) .map(f => { const filePath = path.join(this.exportsDir, f); const stats = fs.statSync(filePath); return { fileName: f, filePath, size: stats.size, created: stats.birthtime, modified: stats.mtime }; }) .sort((a, b) => b.modified.getTime() - a.modified.getTime()); return exports; }, 'FileOperationsService.listExports'); } /** * List available backups */ async listBackups(postId?: number): Promise<any[]> { return ErrorHandler.wrapAsync(async () => { if (!fs.existsSync(this.backupsDir)) { return []; } const files = fs.readdirSync(this.backupsDir); let backups = files .filter(f => f.endsWith('.json')) .filter(f => !postId || f.includes(`page-${postId}-`)) .map(f => { const filePath = path.join(this.backupsDir, f); const stats = fs.statSync(filePath); // Try to extract postId from filename const match = f.match(/backup-page-(\d+)-/); const extractedPostId = match && match[1] ? parseInt(match[1]) : null; return { fileName: f, filePath, postId: extractedPostId, size: stats.size, created: stats.birthtime, modified: stats.mtime }; }) .sort((a, b) => b.modified.getTime() - a.modified.getTime()); return backups; }, 'FileOperationsService.listBackups'); } /** * Delete old backups (cleanup) */ async cleanupOldBackups(daysToKeep: number = 30): Promise<number> { return ErrorHandler.wrapAsync(async () => { logger.info(`Cleaning up backups older than ${daysToKeep} days`); if (!fs.existsSync(this.backupsDir)) { return 0; } const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); const files = fs.readdirSync(this.backupsDir); let deletedCount = 0; for (const file of files) { const filePath = path.join(this.backupsDir, file); const stats = fs.statSync(filePath); if (stats.mtime < cutoffDate) { fs.unlinkSync(filePath); deletedCount++; logger.debug(`Deleted old backup: ${file}`); } } logger.info(`Cleanup complete. Deleted ${deletedCount} old backups`); return deletedCount; }, 'FileOperationsService.cleanupOldBackups'); } /** * Validate imported data structure */ private validateImportData(data: any): void { if (!data.postId || typeof data.postId !== 'number') { throw new MCPError( 'Invalid import data: missing or invalid postId', ErrorCategory.VALIDATION, 'INVALID_STRUCTURE' ); } if (!data.data) { throw new MCPError( 'Invalid import data: missing data field', ErrorCategory.VALIDATION, 'INVALID_STRUCTURE' ); } } /** * Generate checksum for data verification */ private generateChecksum(data: string): string { let hash = 0; for (let i = 0; i < data.length; i++) { const char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36); } /** * Get file info */ async getFileInfo(filePath: string): Promise<any> { return ErrorHandler.wrapAsync(async () => { if (!fs.existsSync(filePath)) { throw new MCPError( `File not found: ${filePath}`, ErrorCategory.FILE_OPERATION, 'FILE_NOT_FOUND' ); } const stats = fs.statSync(filePath); const content = fs.readFileSync(filePath, 'utf-8'); let parsedData = null; try { parsedData = JSON.parse(content); } catch (error) { // Not JSON, that's okay } return { filePath, fileName: path.basename(filePath), size: stats.size, created: stats.birthtime, modified: stats.mtime, isJson: parsedData !== null, postId: parsedData?.postId || null }; }, 'FileOperationsService.getFileInfo'); } }

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/mbrown1837/Ultimate-Elementor-MCP'

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