db = DatabaseFactory::dev(); } /** * Find all backups with optional filters. * * @param array $filters */ public function findAll(array $filters = [], int $limit = 50, int $offset = 0): array { $sql = 'SELECT id, file_path, content_hash, file_size, version, change_type, changed_at, changed_by, reason FROM file_backup_history WHERE 1=1'; $params = []; if (!empty($filters['search'])) { $sql .= ' AND file_path LIKE :search'; $params['search'] = '%' . $filters['search'] . '%'; } if (!empty($filters['change_type'])) { $sql .= ' AND change_type = :change_type'; $params['change_type'] = $filters['change_type']; } $sql .= ' ORDER BY changed_at DESC LIMIT :limit OFFSET :offset'; $stmt = $this->db->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->bindValue('limit', $limit, PDO::PARAM_INT); $stmt->bindValue('offset', $offset, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * Count total backups with filters. * * @param array $filters */ public function count(array $filters = []): int { $sql = 'SELECT COUNT(*) FROM file_backup_history WHERE 1=1'; $params = []; if (!empty($filters['search'])) { $sql .= ' AND file_path LIKE :search'; $params['search'] = '%' . $filters['search'] . '%'; } if (!empty($filters['change_type'])) { $sql .= ' AND change_type = :change_type'; $params['change_type'] = $filters['change_type']; } $stmt = $this->db->prepare($sql); $stmt->execute($params); return (int) $stmt->fetchColumn(); } /** * Find backup by ID. */ public function findById(int $id): ?array { $stmt = $this->db->prepare( 'SELECT * FROM file_backup_history WHERE id = :id' ); $stmt->execute(['id' => $id]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result !== false ? $result : null; } /** * Find all backups for a specific file path. */ public function findByFilePath(string $path): array { $stmt = $this->db->prepare( 'SELECT id, file_path, content_hash, file_size, version, change_type, changed_at, changed_by, reason FROM file_backup_history WHERE file_path = :path ORDER BY version DESC' ); $stmt->execute(['path' => $path]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * Get statistics about backups. * * @return array */ public function getStatistics(): array { $stats = []; // Total backups $stats['total'] = (int) $this->db->query( 'SELECT COUNT(*) FROM file_backup_history' )->fetchColumn(); // Unique files $stats['files'] = (int) $this->db->query( 'SELECT COUNT(DISTINCT file_path) FROM file_backup_history' )->fetchColumn(); // Total versions (max version per file summed) $stats['versions'] = (int) $this->db->query( 'SELECT COALESCE(SUM(max_version), 0) FROM ( SELECT MAX(version) as max_version FROM file_backup_history GROUP BY file_path ) as v' )->fetchColumn(); // Last 24 hours $stats['recent'] = (int) $this->db->query( 'SELECT COUNT(*) FROM file_backup_history WHERE changed_at >= NOW() - INTERVAL 24 HOUR' )->fetchColumn(); // By change type $stats['created'] = (int) $this->db->query( "SELECT COUNT(*) FROM file_backup_history WHERE change_type = 'created'" )->fetchColumn(); $stats['modified'] = (int) $this->db->query( "SELECT COUNT(*) FROM file_backup_history WHERE change_type = 'modified'" )->fetchColumn(); return $stats; } /** * Restore a file from backup. * * @throws \RuntimeException If restore fails */ public function restore(int $id): bool { $backup = $this->findById($id); if ($backup === null) { throw new \RuntimeException('Backup not found'); } $filePath = $backup['file_path']; $content = $backup['file_content']; // Check if directory exists $dir = dirname($filePath); if (!is_dir($dir)) { throw new \RuntimeException("Directory does not exist: {$dir}"); } // Write content back to file $result = file_put_contents($filePath, $content); if ($result === false) { throw new \RuntimeException("Failed to write to file: {$filePath}"); } // Log the restore action $this->logRestore($id, $filePath); return true; } /** * Log restore action to mcp_log. */ private function logRestore(int $backupId, string $filePath): void { $stmt = $this->db->prepare( "INSERT INTO mcp_log (tool, operation, parameters, result, logged_at) VALUES ('backup_restore', 'restore', :params, 'success', NOW())" ); $stmt->execute([ 'params' => json_encode(['backup_id' => $backupId, 'file_path' => $filePath]), ]); } /** * Get content preview (first N lines). */ public function getContentPreview(int $id, int $maxLines = 500): ?string { $backup = $this->findById($id); if ($backup === null || empty($backup['file_content'])) { return null; } $lines = explode("\n", $backup['file_content']); if (count($lines) <= $maxLines) { return $backup['file_content']; } return implode("\n", array_slice($lines, 0, $maxLines)) . "\n\n... (" . (count($lines) - $maxLines) . ' weitere Zeilen)'; } }