Skip to main content
Glama
git-backup.ts18 kB
/** * Git Backup Tool * * Comprehensive backup system for Git repositories. * Supports backup creation, restoration, listing, and verification. * * Operations: backup, restore, list, verify */ import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js'; import { TerminalController } from '../utils/terminal-controller.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import path from 'path'; import fs from 'fs/promises'; import { existsSync } from 'fs'; export interface GitBackupParams extends ToolParams { action: 'backup' | 'restore' | 'list' | 'verify'; // Backup parameters backupPath?: string; // Path for backup storage name?: string; // Backup name/identifier format?: 'tar' | 'zip'; // Backup format compression?: boolean; // Enable compression includeUntracked?: boolean; // Include untracked files // Restore parameters targetPath?: string; // Target path for restoration overwrite?: boolean; // Overwrite existing files // List parameters sortBy?: 'name' | 'date' | 'size'; // Sort criteria // Verify parameters checkIntegrity?: boolean; // Check backup integrity } export class GitBackupTool { private gitExecutor: GitCommandExecutor; private terminal: TerminalController; constructor() { this.gitExecutor = new GitCommandExecutor(); this.terminal = new TerminalController(); } async execute(params: GitBackupParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate required parameters const validation = ParameterValidator.validateToolParams('git-backup', params); if (!validation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Validation failed: ${validation.errors.join(', ')}`, 'backup', { errors: validation.errors }, validation.suggestions ); } // Validate project path exists if (!existsSync(params.projectPath)) { return OperationErrorHandler.createToolError( 'PROJECT_NOT_FOUND', `Project path does not exist: ${params.projectPath}`, 'backup', { projectPath: params.projectPath }, ['Ensure the project path exists and is accessible'] ); } switch (params.action) { case 'backup': return await this.handleBackup(params, startTime); case 'restore': return await this.handleRestore(params, startTime); case 'list': return await this.handleList(params, startTime); case 'verify': return await this.handleVerify(params, startTime); default: return OperationErrorHandler.createToolError( 'INVALID_ACTION', `Invalid action: ${params.action}`, params.action, {}, ['Use one of: backup, restore, list, verify'] ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'BACKUP_TOOL_ERROR', `Git backup tool error: ${errorMessage}`, 'backup', { error: errorMessage } ); } } /** * Handle backup creation operation */ private async handleBackup(params: GitBackupParams, startTime: number): Promise<ToolResult> { try { // Check if it's a Git repository const isGitRepo = await this.gitExecutor.isGitRepository(params.projectPath); if (!isGitRepo) { return OperationErrorHandler.createToolError( 'NOT_GIT_REPOSITORY', 'Project is not a Git repository', 'backup', { projectPath: params.projectPath }, ['Initialize Git repository with: git init'] ); } // Generate backup path if not provided if (!params.backupPath) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const repoName = path.basename(params.projectPath); params.backupPath = path.join(params.projectPath, '..', 'backups', `${repoName}-backup-${timestamp}`); } // Ensure backup directory exists const backupDir = path.dirname(params.backupPath); await fs.mkdir(backupDir, { recursive: true }); const format = params.format || 'tar'; const backupName = params.name || `backup-${Date.now()}`; // Create backup using git archive const result = await this.gitExecutor.createBackup(params.projectPath, params.backupPath, format); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'backup', params.projectPath); } const extension = format === 'zip' ? 'zip' : 'tar.gz'; const finalBackupPath = `${params.backupPath}.${extension}`; // Get backup file stats const stats = await fs.stat(finalBackupPath); // Create backup metadata const metadata = { name: backupName, path: finalBackupPath, format, size: stats.size, created: stats.birthtime.toISOString(), repository: path.basename(params.projectPath), compression: params.compression !== false }; // Save metadata file const metadataPath = `${params.backupPath}.meta.json`; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); return { success: true, data: { message: 'Repository backup created successfully', backup: metadata, output: result.stdout }, metadata: { operation: 'backup', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'BACKUP_CREATE_ERROR', `Failed to create backup: ${errorMessage}`, 'backup', { error: errorMessage, backupPath: params.backupPath } ); } } /** * Handle backup restoration operation */ private async handleRestore(params: GitBackupParams, startTime: number): Promise<ToolResult> { try { if (!params.backupPath) { return OperationErrorHandler.createToolError( 'MISSING_BACKUP_PATH', 'Backup path is required for restore operation', 'restore', {}, ['Provide backupPath parameter with the path to the backup file'] ); } // Check if backup file exists if (!existsSync(params.backupPath)) { return OperationErrorHandler.createToolError( 'BACKUP_NOT_FOUND', `Backup file not found: ${params.backupPath}`, 'restore', { backupPath: params.backupPath }, ['Ensure the backup file exists and path is correct'] ); } const targetPath = params.targetPath || params.projectPath; // Check if target directory exists and handle overwrite if (existsSync(targetPath) && !params.overwrite) { return OperationErrorHandler.createToolError( 'TARGET_EXISTS', `Target directory exists: ${targetPath}`, 'restore', { targetPath }, ['Use overwrite: true to overwrite existing directory', 'Choose a different target path'] ); } // Ensure target directory exists await fs.mkdir(targetPath, { recursive: true }); // Determine backup format from file extension const isZip = params.backupPath.endsWith('.zip'); const format = isZip ? 'zip' : 'tar'; // Extract backup let result: GitCommandResult; if (format === 'zip') { // Use unzip command for zip files result = await this.terminal.executeCommand('unzip', ['-o', params.backupPath, '-d', targetPath]); } else { // Use tar command for tar.gz files result = await this.terminal.executeCommand('tar', ['-xzf', params.backupPath, '-C', targetPath]); } if (!result.success) { return OperationErrorHandler.createToolError( 'RESTORE_ERROR', `Failed to extract backup: ${result.stderr}`, 'restore', { backupPath: params.backupPath, targetPath } ); } // Load backup metadata if available const metadataPath = params.backupPath.replace(/\.(zip|tar\.gz)$/, '.meta.json'); let metadata = null; if (existsSync(metadataPath)) { try { const metadataContent = await fs.readFile(metadataPath, 'utf-8'); metadata = JSON.parse(metadataContent); } catch (error) { // Metadata file exists but couldn't be parsed, continue without it } } return { success: true, data: { message: 'Backup restored successfully', backupPath: params.backupPath, targetPath, format, metadata, output: result.stdout }, metadata: { operation: 'restore', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'RESTORE_ERROR', `Failed to restore backup: ${errorMessage}`, 'restore', { error: errorMessage, backupPath: params.backupPath } ); } } /** * Handle backup listing operation */ private async handleList(params: GitBackupParams, startTime: number): Promise<ToolResult> { try { const backupDir = params.backupPath || path.join(params.projectPath, '..', 'backups'); if (!existsSync(backupDir)) { return { success: true, data: { message: 'No backup directory found', backups: [], backupDir }, metadata: { operation: 'list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } const files = await fs.readdir(backupDir); const backups = []; for (const file of files) { if (file.endsWith('.tar.gz') || file.endsWith('.zip')) { const filePath = path.join(backupDir, file); const stats = await fs.stat(filePath); // Try to load metadata const metadataPath = filePath.replace(/\.(zip|tar\.gz)$/, '.meta.json'); let metadata = null; if (existsSync(metadataPath)) { try { const metadataContent = await fs.readFile(metadataPath, 'utf-8'); metadata = JSON.parse(metadataContent); } catch (error) { // Continue without metadata if parsing fails } } backups.push({ name: file, path: filePath, size: stats.size, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), format: file.endsWith('.zip') ? 'zip' : 'tar', metadata }); } } // Sort backups const sortBy = params.sortBy || 'date'; backups.sort((a, b) => { switch (sortBy) { case 'name': return a.name.localeCompare(b.name); case 'size': return b.size - a.size; case 'date': default: return new Date(b.created).getTime() - new Date(a.created).getTime(); } }); return { success: true, data: { message: `Found ${backups.length} backup(s)`, backups, backupDir, sortBy }, metadata: { operation: 'list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'LIST_ERROR', `Failed to list backups: ${errorMessage}`, 'list', { error: errorMessage, backupPath: params.backupPath } ); } } /** * Handle backup verification operation */ private async handleVerify(params: GitBackupParams, startTime: number): Promise<ToolResult> { try { if (!params.backupPath) { return OperationErrorHandler.createToolError( 'MISSING_BACKUP_PATH', 'Backup path is required for verify operation', 'verify', {}, ['Provide backupPath parameter with the path to the backup file'] ); } if (!existsSync(params.backupPath)) { return OperationErrorHandler.createToolError( 'BACKUP_NOT_FOUND', `Backup file not found: ${params.backupPath}`, 'verify', { backupPath: params.backupPath }, ['Ensure the backup file exists and path is correct'] ); } const stats = await fs.stat(params.backupPath); const isZip = params.backupPath.endsWith('.zip'); const format = isZip ? 'zip' : 'tar'; // Basic file verification const verification = { exists: true, readable: true, size: stats.size, format, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), integrity: 'unknown' as 'valid' | 'invalid' | 'unknown' }; // Test archive integrity if requested if (params.checkIntegrity) { let result: GitCommandResult; if (format === 'zip') { // Test zip file integrity result = await this.terminal.executeCommand('unzip', ['-t', params.backupPath]); } else { // Test tar.gz file integrity result = await this.terminal.executeCommand('tar', ['-tzf', params.backupPath]); } verification.integrity = result.success ? 'valid' : 'invalid'; } // Load and verify metadata if available const metadataPath = params.backupPath.replace(/\.(zip|tar\.gz)$/, '.meta.json'); let metadata = null; let metadataValid = false; if (existsSync(metadataPath)) { try { const metadataContent = await fs.readFile(metadataPath, 'utf-8'); metadata = JSON.parse(metadataContent); metadataValid = true; // Verify metadata consistency if (metadata.size !== stats.size) { metadataValid = false; } } catch (error) { metadataValid = false; } } const isValid = verification.integrity !== 'invalid' && (metadata ? metadataValid : true); return { success: true, data: { message: isValid ? 'Backup verification passed' : 'Backup verification failed', valid: isValid, verification, metadata: metadata ? { ...metadata, valid: metadataValid } : null }, metadata: { operation: 'verify', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'VERIFY_ERROR', `Failed to verify backup: ${errorMessage}`, 'verify', { error: errorMessage, backupPath: params.backupPath } ); } } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-backup', description: 'Comprehensive backup system for Git repositories. Supports backup creation, restoration, listing, and verification.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['backup', 'restore', 'list', 'verify'], description: 'The backup operation to perform' }, projectPath: { type: 'string', description: 'Path to the Git repository (required)' }, backupPath: { type: 'string', description: 'Path for backup storage or backup file to restore/verify' }, name: { type: 'string', description: 'Backup name/identifier (for backup operation)' }, format: { type: 'string', enum: ['tar', 'zip'], description: 'Backup format (default: tar)' }, compression: { type: 'boolean', description: 'Enable compression (default: true)' }, includeUntracked: { type: 'boolean', description: 'Include untracked files in backup (default: false)' }, targetPath: { type: 'string', description: 'Target path for restoration (for restore operation)' }, overwrite: { type: 'boolean', description: 'Overwrite existing files during restore (default: false)' }, sortBy: { type: 'string', enum: ['name', 'date', 'size'], description: 'Sort criteria for listing backups (default: date)' }, checkIntegrity: { type: 'boolean', description: 'Check backup integrity during verification (default: false)' } }, required: ['action', 'projectPath'] } }; } }

Implementation Reference

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/Andre-Buzeli/git-mcp'

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