Files
deskhub/DeskHub-Webseite/includes/backup.php
T
2026-10-16 19:12:15 +00:00

224 lines
9.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Backup-Hilfsfunktion – wird von update.php, backup.php und den Cron-Jobs genutzt.
*
* Ein Backup ist eine ZIP-Datei mit
* - allen Programm- und Datendateien (ohne Backups, Entwickler-Artefakte),
* - einem konsistenten Datenbank-Abzug: database_backup.sqlite (SQLite, per VACUUM INTO) oder database_backup.sql (MySQL),
* - backup_manifest.json (Zeitpunkt, Datenbanktyp, Programmversion, Anzahl Tabellen).
* Wiederherstellen und Prüfen: includes/backup_restore.php.
*/
require_once __DIR__ . '/simple_zip.php';
/** Pfade, die nie ins Backup gehören (Entwickler-Artefakte und Zwischenspeicher). */
function backup_excluded_fragments(): array
{
return ['/node_modules/', '/.git/', '/desktop/dist/', '/mobile/build/', '/mobile/artifacts/', '/plugin-webseite/storage/', '/plugin-webseite/schluessel/'];
}
// ── Backup erstellen ───────────────────────────────────────────────�
/**
* @param string|null $rootDir Ordner, dessen Dateien gesichert werden (Standard: DeskHub-Hauptordner)
* @param string|null $backupDir Zielordner (Standard: backups/ im Hauptordner)
* @param PDO|null $pdo Datenbankverbindung (Standard: get_db())
*/
function create_backup(?string $rootDir = null, ?string $backupDir = null, ?PDO $pdo = null): array
{
$rootDir = realpath($rootDir ?? __DIR__ . '/..');
$backupDir = $backupDir ?? $rootDir . '/backups';
$pdo = $pdo ?? get_db();
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
}
// .htaccess zum Schutz des Backup-Ordners
$htaccess = $backupDir . '/.htaccess';
if (!file_exists($htaccess)) {
file_put_contents($htaccess, "Require all denied
Order deny,allow
Deny from all
");
}
$backupName = 'backup_' . date('Y-m-d_H-i-s') . '.zip';
$backupPath = $backupDir . '/' . $backupName;
if (file_exists($backupPath)) { // zwei Backups in derselben Sekunde
$backupName = 'backup_' . date('Y-m-d_H-i-s') . '_' . substr(bin2hex(random_bytes(2)), 0, 3) . '.zip';
$backupPath = $backupDir . '/' . $backupName;
}
// Zeitlimit erhöhen für große Backups
@set_time_limit(300);
@ini_set('memory_limit', '256M');
// php_zip automatisch aktivieren falls nicht geladen
if (!class_exists('ZipArchive')) {
$activated = false;
// 1. Dynamisch laden (funktioniert wenn dl() erlaubt)
if (function_exists('dl') && ini_get('enable_dl')) {
$activated = @dl('php_zip.dll') || @dl('zip.so') || @dl('zip');
}
// 2. php.ini dauerhaft anpassen (für nächsten Start)
if (!$activated) {
$phpIni = php_ini_loaded_file();
if ($phpIni && is_writable($phpIni)) {
$ini = file_get_contents($phpIni);
$patched = preg_replace('/^[ \t]*;[ \t]*(extension[ \t]*=[ \t]*(?:php_)?zip(?:\.dll|\.so)?)/mi', '$1', $ini);
if ($patched !== $ini) {
file_put_contents($phpIni, $patched);
// Hinweis speichern dass Neustart nötig ist
file_put_contents($backupDir . '/.restart_required', '1');
}
}
}
}
// Datenbank-Abzug: SQLite-Datei wird NICHT als laufende Datei kopiert, sondern konsistent per VACUUM INTO gesichert
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$liveDb = null;
if ($driver === 'sqlite') {
$row = $pdo->query('PRAGMA database_list')->fetch(PDO::FETCH_ASSOC);
$liveDb = !empty($row['file']) ? realpath($row['file']) : null;
}
// Alle Dateien sammeln
$exclude = [realpath($backupDir) ?: $backupDir];
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($rootDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
$files = [];
foreach ($iter as $file) {
if (!$file->isFile()) continue;
$filePath = $file->getRealPath();
$norm = str_replace('\\', '/', $filePath);
$skip = false;
foreach ($exclude as $ex) {
if ($ex && strpos($filePath, $ex) === 0) { $skip = true; break; }
}
foreach (backup_excluded_fragments() as $frag) {
if (str_contains($norm, $frag)) { $skip = true; break; }
}
if ($liveDb && ($filePath === $liveDb || $filePath === $liveDb . '-wal' || $filePath === $liveDb . '-shm' || $filePath === $liveDb . '-journal')) $skip = true;
if ($skip) continue;
$files[$filePath] = str_replace(chr(92), '/', substr($filePath, strlen($rootDir) + 1)); // im ZIP immer mit /
}
// Abzug erzeugen
$sqlDump = null; // MySQL: Text
$sqlFile = null; // MySQL: Dateiname im ZIP
$sqliteSnapshot = null;
$tableCount = 0;
$dbError = null;
if ($driver === 'sqlite') {
$snapshot = tempnam(sys_get_temp_dir(), 'dhbk');
@unlink($snapshot);
try {
$pdo->exec('VACUUM INTO ' . $pdo->quote($snapshot));
$sqliteSnapshot = $snapshot;
} catch (Throwable $e) {
// Ältere SQLite-Version ohne VACUUM INTO: Datei kopieren (weniger sicher, aber besser als nichts)
if ($liveDb && @copy($liveDb, $snapshot)) $sqliteSnapshot = $snapshot;
else $dbError = 'SQLite-Abzug fehlgeschlagen: ' . $e->getMessage();
}
if ($sqliteSnapshot) {
$tableCount = (int) $pdo->query("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")->fetchColumn();
}
} elseif ($driver === 'mysql') {
try {
$dbName = (string) $pdo->query('SELECT DATABASE()')->fetchColumn();
$dump = "-- DeskHub MySQL Backup\n-- Erstellt: " . date('Y-m-d H:i:s') . "\n-- Datenbank: " . $dbName . "\n\nSET FOREIGN_KEY_CHECKS=0;\n\n";
// Alle Tabellen auslesen
$tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
$tableCount = count($tables);
foreach ($tables as $table) {
// CREATE TABLE
$create = $pdo->query("SHOW CREATE TABLE `$table`")->fetch();
$dump .= "\n-- Tabelle: $table\n";
$dump .= "DROP TABLE IF EXISTS `$table`;\n";
$dump .= $create['Create Table'] . ";\n\n";
// Daten
$rows = $pdo->query("SELECT * FROM `$table`")->fetchAll(PDO::FETCH_ASSOC);
if ($rows) {
$cols = '`' . implode('`, `', array_keys($rows[0])) . '`';
$dump .= "INSERT INTO `$table` ($cols) VALUES\n";
$vals = [];
foreach ($rows as $row) {
$escaped = array_map(fn($v) => $v === null ? 'NULL' : $pdo->quote((string)$v), $row);
$vals[] = '(' . implode(', ', $escaped) . ')';
}
$dump .= implode(",\n", $vals) . ";\n\n";
}
}
$dump .= "SET FOREIGN_KEY_CHECKS=1;\n";
$sqlFile = 'database_backup.sql';
$sqlDump = $dump;
} catch (Throwable $e) {
$dbError = 'MySQL-Abzug fehlgeschlagen: ' . $e->getMessage();
}
}
// Ohne Datenbank-Abzug ist es kein brauchbares Backup: lieber Fehler melden als ein Backup vortäuschen
if ($dbError !== null) {
if ($sqliteSnapshot) @unlink($sqliteSnapshot);
return ['ok' => false, 'error' => $dbError];
}
$manifest = json_encode([
'created' => date('Y-m-d H:i:s'), 'driver' => $driver, 'version' => defined('DESKHUB_VERSION') ? DESKHUB_VERSION : null,
'database' => $sqliteSnapshot ? 'database_backup.sqlite' : $sqlFile, 'tables' => $tableCount, 'files' => count($files),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// ZIP erstellen – ZipArchive wenn verfügbar, sonst eigene Implementierung
try {
if (class_exists('ZipArchive')) {
$zip = new ZipArchive();
if ($zip->open($backupPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return ['ok' => false, 'error' => 'Backup-ZIP konnte nicht erstellt werden.'];
}
foreach ($files as $filePath => $relativePath) {
$zip->addFile($filePath, $relativePath);
}
if ($sqlDump && $sqlFile) $zip->addFromString($sqlFile, $sqlDump);
if ($sqliteSnapshot) $zip->addFile($sqliteSnapshot, 'database_backup.sqlite');
$zip->addFromString('backup_manifest.json', $manifest);
$zip->close();
} else {
// Eigene ZIP-Implementierung (kein php_zip nötig)
$zip = new SimpleZip($backupPath);
foreach ($files as $filePath => $relativePath) {
$zip->addFile($filePath, $relativePath);
}
if ($sqlDump && $sqlFile) $zip->addFromString($sqlFile, $sqlDump);
if ($sqliteSnapshot) $zip->addFromString('database_backup.sqlite', (string) file_get_contents($sqliteSnapshot));
$zip->addFromString('backup_manifest.json', $manifest);
$zip->close();
}
} catch (Exception $e) {
if ($sqliteSnapshot) @unlink($sqliteSnapshot);
return ['ok' => false, 'error' => 'Backup fehlgeschlagen: ' . $e->getMessage()];
}
if ($sqliteSnapshot) @unlink($sqliteSnapshot);
if (!file_exists($backupPath)) {
return ['ok' => false, 'error' => 'Backup-Datei wurde nicht erstellt.'];
}
return [
'ok' => true,
'filename' => $backupName,
'path' => $backupPath,
'files' => count($files),
'size' => round(filesize($backupPath) / 1024 / 1024, 2),
];
}