conn = DriverManager::getConnection([ 'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop' ]); $this->config = new Configuration(); $this->backupPath = getenv('BACKUP_PATH') ?: __DIR__ . '/../../backups'; $this->maxBackups = intval(getenv('MAX_BACKUPS') ?: 10); $this->compressionLevel = intval(getenv('BACKUP_COMPRESSION') ?: 6); // Backup-Verzeichnis erstellen if (!is_dir($this->backupPath)) { mkdir($this->backupPath, 0755, true); } } /** * Vollständiges Backup erstellen */ public function createFullBackup($description = '') { try { $timestamp = date('Y-m-d_H-i-s'); $backupName = "webshop_backup_{$timestamp}"; $backupDir = $this->backupPath . '/' . $backupName; if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } // Datenbank-Backup $dbBackup = $this->createDatabaseBackup($backupDir); // File-Backup $fileBackup = $this->createFileBackup($backupDir); // Konfigurations-Backup $configBackup = $this->createConfigBackup($backupDir); // Backup-Metadaten $metadata = [ 'timestamp' => $timestamp, 'description' => $description, 'version' => $this->config->get('WEBSHOP_VERSION'), 'database_size' => $dbBackup['size'], 'files_size' => $fileBackup['size'], 'config_size' => $configBackup['size'], 'total_size' => $dbBackup['size'] + $fileBackup['size'] + $configBackup['size'], 'checksum' => $this->calculateBackupChecksum($backupDir) ]; file_put_contents($backupDir . '/metadata.json', json_encode($metadata, JSON_PRETTY_PRINT)); // Backup komprimieren $archivePath = $this->compressBackup($backupDir, $backupName); // Alte Backups bereinigen $this->cleanupOldBackups(); // Cloud-Backup (falls konfiguriert) $this->uploadToCloud($archivePath); return [ 'success' => true, 'backup_name' => $backupName, 'archive_path' => $archivePath, 'size' => filesize($archivePath), 'metadata' => $metadata ]; } catch (Exception $e) { error_log('Backup error: ' . $e->getMessage()); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Datenbank-Backup erstellen */ private function createDatabaseBackup($backupDir) { $dbConfig = parse_url(getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'); $host = $dbConfig['host'] ?? 'localhost'; $port = $dbConfig['port'] ?? 3306; $database = ltrim($dbConfig['path'] ?? 'webshop', '/'); $username = $dbConfig['user'] ?? 'root'; $password = $dbConfig['pass'] ?? 'password'; $sqlFile = $backupDir . '/database.sql'; // mysqldump Kommando $command = sprintf( 'mysqldump --host=%s --port=%d --user=%s --password=%s --single-transaction --routines --triggers %s > %s', escapeshellarg($host), $port, escapeshellarg($username), escapeshellarg($password), escapeshellarg($database), escapeshellarg($sqlFile) ); exec($command, $output, $returnCode); if ($returnCode !== 0) { throw new Exception('Database backup failed'); } return [ 'file' => $sqlFile, 'size' => filesize($sqlFile) ]; } /** * File-Backup erstellen */ private function createFileBackup($backupDir) { $filesDir = $backupDir . '/files'; mkdir($filesDir, 0755, true); $sourceDirs = [ 'uploads' => __DIR__ . '/../../public/uploads', 'images' => __DIR__ . '/../../public/img', 'templates' => __DIR__ . '/../../templates', 'config' => __DIR__ . '/../../config' ]; $totalSize = 0; foreach ($sourceDirs as $name => $sourceDir) { if (is_dir($sourceDir)) { $destDir = $filesDir . '/' . $name; $this->copyDirectory($sourceDir, $destDir); $totalSize += $this->getDirectorySize($destDir); } } return [ 'dir' => $filesDir, 'size' => $totalSize ]; } /** * Konfigurations-Backup erstellen */ private function createConfigBackup($backupDir) { $configFile = $backupDir . '/config.json'; $config = [ 'database' => [ 'url' => getenv('DATABASE_URL'), 'host' => getenv('DB_HOST'), 'port' => getenv('DB_PORT'), 'database' => getenv('DB_NAME'), 'username' => getenv('DB_USER') ], 'redis' => [ 'host' => getenv('REDIS_HOST'), 'port' => getenv('REDIS_PORT') ], 'app' => [ 'environment' => getenv('APP_ENV'), 'debug' => getenv('APP_DEBUG'), 'secret' => getenv('APP_SECRET') ], 'backup' => [ 'path' => $this->backupPath, 'max_backups' => $this->maxBackups, 'compression_level' => $this->compressionLevel ] ]; file_put_contents($configFile, json_encode($config, JSON_PRETTY_PRINT)); return [ 'file' => $configFile, 'size' => filesize($configFile) ]; } /** * Backup komprimieren */ private function compressBackup($backupDir, $backupName) { $archivePath = $this->backupPath . '/' . $backupName . '.tar.gz'; $command = sprintf( 'tar -czf %s -C %s .', escapeshellarg($archivePath), escapeshellarg($backupDir) ); exec($command, $output, $returnCode); if ($returnCode !== 0) { throw new Exception('Backup compression failed'); } // Backup-Verzeichnis löschen $this->removeDirectory($backupDir); return $archivePath; } /** * Backup-Checksum berechnen */ private function calculateBackupChecksum($backupDir) { $files = []; $this->getAllFiles($backupDir, $files); $checksums = []; foreach ($files as $file) { $checksums[] = hash_file('sha256', $file); } return hash('sha256', implode('', $checksums)); } /** * Alte Backups bereinigen */ private function cleanupOldBackups() { $backups = glob($this->backupPath . '/webshop_backup_*.tar.gz'); if (count($backups) > $this->maxBackups) { // Nach Datum sortieren (älteste zuerst) usort($backups, function($a, $b) { return filemtime($a) - filemtime($b); }); $toDelete = array_slice($backups, 0, count($backups) - $this->maxBackups); foreach ($toDelete as $backup) { unlink($backup); } } } /** * Cloud-Backup hochladen */ private function uploadToCloud($archivePath) { $cloudType = getenv('CLOUD_BACKUP_TYPE'); if (!$cloudType) { return; // Kein Cloud-Backup konfiguriert } switch ($cloudType) { case 's3': $this->uploadToS3($archivePath); break; case 'ftp': $this->uploadToFTP($archivePath); break; case 'sftp': $this->uploadToSFTP($archivePath); break; } } /** * S3-Backup */ private function uploadToS3($archivePath) { $bucket = getenv('AWS_S3_BUCKET'); $region = getenv('AWS_REGION') ?: 'us-east-1'; if (!$bucket) { return; } $command = sprintf( 'aws s3 cp %s s3://%s/backups/ --region %s', escapeshellarg($archivePath), escapeshellarg($bucket), escapeshellarg($region) ); exec($command, $output, $returnCode); if ($returnCode !== 0) { error_log('S3 upload failed'); } } /** * FTP-Backup */ private function uploadToFTP($archivePath) { $host = getenv('FTP_HOST'); $username = getenv('FTP_USERNAME'); $password = getenv('FTP_PASSWORD'); $path = getenv('FTP_PATH') ?: '/backups'; if (!$host || !$username || !$password) { return; } $ftp = ftp_connect($host); if (!$ftp) { error_log('FTP connection failed'); return; } if (!ftp_login($ftp, $username, $password)) { error_log('FTP login failed'); ftp_close($ftp); return; } $remoteFile = $path . '/' . basename($archivePath); if (!ftp_put($ftp, $remoteFile, $archivePath, FTP_BINARY)) { error_log('FTP upload failed'); } ftp_close($ftp); } /** * SFTP-Backup */ private function uploadToSFTP($archivePath) { $host = getenv('SFTP_HOST'); $username = getenv('SFTP_USERNAME'); $password = getenv('SFTP_PASSWORD'); $path = getenv('SFTP_PATH') ?: '/backups'; if (!$host || !$username || !$password) { return; } $command = sprintf( 'sshpass -p %s scp %s %s@%s:%s/', escapeshellarg($password), escapeshellarg($archivePath), escapeshellarg($username), escapeshellarg($host), escapeshellarg($path) ); exec($command, $output, $returnCode); if ($returnCode !== 0) { error_log('SFTP upload failed'); } } /** * Backup wiederherstellen */ public function restoreBackup($backupPath, $options = []) { try { $tempDir = $this->backupPath . '/temp_restore_' . uniqid(); mkdir($tempDir, 0755, true); // Backup entpacken $command = sprintf( 'tar -xzf %s -C %s', escapeshellarg($backupPath), escapeshellarg($tempDir) ); exec($command, $output, $returnCode); if ($returnCode !== 0) { throw new Exception('Failed to extract backup'); } // Metadaten prüfen $metadataFile = $tempDir . '/metadata.json'; if (!file_exists($metadataFile)) { throw new Exception('Invalid backup format'); } $metadata = json_decode(file_get_contents($metadataFile), true); // Checksum validieren if (!$this->validateBackupChecksum($tempDir, $metadata['checksum'])) { throw new Exception('Backup checksum validation failed'); } $restored = []; // Datenbank wiederherstellen if ($options['restore_database'] ?? true) { $restored['database'] = $this->restoreDatabase($tempDir); } // Files wiederherstellen if ($options['restore_files'] ?? true) { $restored['files'] = $this->restoreFiles($tempDir); } // Konfiguration wiederherstellen if ($options['restore_config'] ?? false) { $restored['config'] = $this->restoreConfig($tempDir); } // Temp-Verzeichnis löschen $this->removeDirectory($tempDir); return [ 'success' => true, 'restored' => $restored, 'metadata' => $metadata ]; } catch (Exception $e) { if (isset($tempDir) && is_dir($tempDir)) { $this->removeDirectory($tempDir); } return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Datenbank wiederherstellen */ private function restoreDatabase($backupDir) { $sqlFile = $backupDir . '/database.sql'; if (!file_exists($sqlFile)) { throw new Exception('Database backup not found'); } $dbConfig = parse_url(getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'); $host = $dbConfig['host'] ?? 'localhost'; $port = $dbConfig['port'] ?? 3306; $database = ltrim($dbConfig['path'] ?? 'webshop', '/'); $username = $dbConfig['user'] ?? 'root'; $password = $dbConfig['pass'] ?? 'password'; $command = sprintf( 'mysql --host=%s --port=%d --user=%s --password=%s %s < %s', escapeshellarg($host), $port, escapeshellarg($username), escapeshellarg($password), escapeshellarg($database), escapeshellarg($sqlFile) ); exec($command, $output, $returnCode); if ($returnCode !== 0) { throw new Exception('Database restore failed'); } return true; } /** * Files wiederherstellen */ private function restoreFiles($backupDir) { $filesDir = $backupDir . '/files'; if (!is_dir($filesDir)) { return false; } $restored = []; $targetDirs = [ 'uploads' => __DIR__ . '/../../public/uploads', 'images' => __DIR__ . '/../../public/img', 'templates' => __DIR__ . '/../../templates', 'config' => __DIR__ . '/../../config' ]; foreach ($targetDirs as $name => $targetDir) { $sourceDir = $filesDir . '/' . $name; if (is_dir($sourceDir)) { if (is_dir($targetDir)) { $this->removeDirectory($targetDir); } $this->copyDirectory($sourceDir, $targetDir); $restored[$name] = true; } } return $restored; } /** * Konfiguration wiederherstellen */ private function restoreConfig($backupDir) { $configFile = $backupDir . '/config.json'; if (!file_exists($configFile)) { return false; } $config = json_decode(file_get_contents($configFile), true); // Nur sichere Konfigurationen wiederherstellen $safeConfig = [ 'backup' => $config['backup'] ?? [], 'app' => array_intersect_key($config['app'] ?? [], array_flip(['environment', 'debug'])) ]; // .env Datei aktualisieren $envFile = __DIR__ . '/../../.env'; if (file_exists($envFile)) { $envContent = file_get_contents($envFile); foreach ($safeConfig['app'] as $key => $value) { $envContent = preg_replace( "/^{$key}=.*/m", "{$key}={$value}", $envContent ); } file_put_contents($envFile, $envContent); } return $safeConfig; } /** * Backup-Liste abrufen */ public function getBackupList() { $backups = glob($this->backupPath . '/webshop_backup_*.tar.gz'); $backupList = []; foreach ($backups as $backup) { $filename = basename($backup); $timestamp = str_replace(['webshop_backup_', '.tar.gz'], '', $filename); $backupList[] = [ 'filename' => $filename, 'path' => $backup, 'timestamp' => $timestamp, 'size' => filesize($backup), 'date' => date('Y-m-d H:i:s', filemtime($backup)) ]; } // Nach Datum sortieren (neueste zuerst) usort($backupList, function($a, $b) { return strtotime($b['date']) - strtotime($a['date']); }); return $backupList; } /** * Backup-Status prüfen */ public function checkBackupStatus() { $backups = $this->getBackupList(); $latestBackup = $backups[0] ?? null; $status = [ 'backup_enabled' => true, 'last_backup' => $latestBackup ? $latestBackup['date'] : null, 'backup_count' => count($backups), 'total_size' => array_sum(array_column($backups, 'size')), 'backup_path' => $this->backupPath, 'max_backups' => $this->maxBackups ]; // Prüfe ob Backup älter als 24 Stunden ist if ($latestBackup) { $lastBackupTime = strtotime($latestBackup['date']); $status['backup_age_hours'] = (time() - $lastBackupTime) / 3600; $status['backup_needed'] = $status['backup_age_hours'] > 24; } else { $status['backup_needed'] = true; } return $status; } /** * Hilfsfunktionen */ private function copyDirectory($source, $destination) { if (!is_dir($destination)) { mkdir($destination, 0755, true); } $dir = opendir($source); while (($file = readdir($dir)) !== false) { if ($file != '.' && $file != '..') { $sourcePath = $source . '/' . $file; $destPath = $destination . '/' . $file; if (is_dir($sourcePath)) { $this->copyDirectory($sourcePath, $destPath); } else { copy($sourcePath, $destPath); } } } closedir($dir); } private function removeDirectory($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; if (is_dir($path)) { $this->removeDirectory($path); } else { unlink($path); } } rmdir($dir); } private function getDirectorySize($dir) { $size = 0; $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; if (is_dir($path)) { $size += $this->getDirectorySize($path); } else { $size += filesize($path); } } return $size; } private function getAllFiles($dir, &$files) { $items = array_diff(scandir($dir), ['.', '..']); foreach ($items as $item) { $path = $dir . '/' . $item; if (is_dir($path)) { $this->getAllFiles($path, $files); } else { $files[] = $path; } } } private function validateBackupChecksum($backupDir, $expectedChecksum) { $actualChecksum = $this->calculateBackupChecksum($backupDir); return hash_equals($expectedChecksum, $actualChecksum); } }