712 lines
20 KiB
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);
|
|
}
|
|
}
|