Newwebshop/app/Core/Backup.php

712 lines
20 KiB
PHP

<?php
/**
* Copyright seit 2024 Webshop System
*
* Backup Core-Klasse für das Webshop-System
*
* @author Webshop System
* @license GPL v3
*/
namespace App\Core;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class Backup
{
private $conn;
private $config;
private $backupPath;
private $maxBackups;
private $compressionLevel;
public function __construct()
{
$this->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);
}
}