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