// 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');
}
}