Upload via GUI (267 Dateien)

This commit is contained in:
2026-10-19 18:05:57 +00:00
parent f3e53e6182
commit c800be9693
12 changed files with 2694 additions and 2082 deletions
+62 -7
View File
@@ -22,9 +22,18 @@ function backup_excluded_fragments(): array
* @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())
* @param array $options Optional: 'with_database' (bool, Standard true), 'with_files' (bool, Standard true),
* 'only' (Liste von Pfaden relativ zum Hauptordner, nur diese Dateien/Ordner sichern),
* 'exclude' (Liste von Pfaden relativ zum Hauptordner, die nicht gesichert werden),
* 'verify' (bool, Standard true: Backup sofort automatisch auf Wiederherstellbarkeit prüfen und das Ergebnis merken)
*/
function create_backup(?string $rootDir = null, ?string $backupDir = null, ?PDO $pdo = null): array
function create_backup(?string $rootDir = null, ?string $backupDir = null, ?PDO $pdo = null, array $options = []): array
{
$withDb = $options['with_database'] ?? true;
$withFiles = $options['with_files'] ?? true;
$norm = fn(string $p) => trim(str_replace(chr(92), '/', $p), '/');
$onlyRel = array_values(array_filter(array_map($norm, (array) ($options['only'] ?? []))));
$excludeRel = array_values(array_filter(array_map($norm, (array) ($options['exclude'] ?? []))));
$rootDir = realpath($rootDir ?? __DIR__ . '/..');
$backupDir = $backupDir ?? $rootDir . '/backups';
$pdo = $pdo ?? get_db();
@@ -93,7 +102,7 @@ Deny from all
);
$files = [];
foreach ($iter as $file) {
foreach ($withFiles ? $iter : [] as $file) {
if (!$file->isFile()) continue;
$filePath = $file->getRealPath();
$norm = str_replace('\\', '/', $filePath);
@@ -106,7 +115,18 @@ Deny from all
}
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 /
$rel = str_replace(chr(92), '/', substr($filePath, strlen($rootDir) + 1)); // im ZIP immer mit /
// Auswahl und Ausschlüsse (relativ zum Hauptordner, ganze Ordner oder einzelne Dateien)
$under = fn(array $list) => (bool) array_filter($list, fn($p) => $rel === $p || str_starts_with($rel, $p . '/'));
if ($onlyRel && !$under($onlyRel)) continue;
if ($excludeRel && $under($excludeRel)) continue;
$files[$filePath] = $rel;
}
// Dateien, die der Webserver-Benutzer nicht lesen kann (fremde Besitzer/Rechte), würden das ganze Backup scheitern lassen: überspringen und melden
$skipped = [];
foreach ($files as $filePath => $relativePath) {
if (!is_readable($filePath)) { $skipped[] = $relativePath; unset($files[$filePath]); }
}
// Abzug erzeugen
@@ -116,7 +136,7 @@ Deny from all
$tableCount = 0;
$dbError = null;
if ($driver === 'sqlite') {
if ($withDb && $driver === 'sqlite') {
$snapshot = tempnam(sys_get_temp_dir(), 'dhbk');
@unlink($snapshot);
try {
@@ -130,7 +150,7 @@ Deny from all
if ($sqliteSnapshot) {
$tableCount = (int) $pdo->query("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")->fetchColumn();
}
} elseif ($driver === 'mysql') {
} elseif ($withDb && $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";
@@ -175,7 +195,7 @@ Deny from all
$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),
'database' => $sqliteSnapshot ? 'database_backup.sqlite' : $sqlFile, 'tables' => $tableCount, 'files' => count($files), 'skipped' => array_slice($skipped, 0, 50),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// ZIP erstellen – ZipArchive wenn verfügbar, sonst eigene Implementierung
@@ -191,7 +211,32 @@ Deny from all
if ($sqlDump && $sqlFile) $zip->addFromString($sqlFile, $sqlDump);
if ($sqliteSnapshot) $zip->addFile($sqliteSnapshot, 'database_backup.sqlite');
$zip->addFromString('backup_manifest.json', $manifest);
$zip->close();
$closed = @$zip->close();
if (!$closed || !file_exists($backupPath)) {
$zipReason = trim((string) $zip->getStatusString());
// Zweiter Versuch: nur Dateien, die sich wirklich öffnen lassen
@unlink($backupPath);
$retry = new ZipArchive();
if ($retry->open($backupPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
foreach ($files as $filePath => $relativePath) {
$h = @fopen($filePath, 'rb');
if ($h === false) { $skipped[] = $relativePath; continue; }
fclose($h);
$retry->addFile($filePath, $relativePath);
}
if ($sqlDump && $sqlFile) $retry->addFromString($sqlFile, $sqlDump);
if ($sqliteSnapshot) $retry->addFile($sqliteSnapshot, 'database_backup.sqlite');
$retry->addFromString('backup_manifest.json', 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) - count($skipped), 'skipped' => array_slice($skipped, 0, 50)], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
@$retry->close();
if (!file_exists($backupPath)) $zipReason = trim($zipReason . ' ' . $retry->getStatusString());
}
if (!file_exists($backupPath)) {
if ($sqliteSnapshot) @unlink($sqliteSnapshot);
$free = @disk_free_space($backupDir);
return ['ok' => false, 'error' => 'Backup-Datei wurde nicht erstellt' . ($zipReason !== '' ? ' (' . $zipReason . ')' : '')
. '. Prüfen: Ist der Ordner „' . basename($backupDir) . '“ beschreibbar' . ($free !== false ? ' und ist genug Speicherplatz frei (aktuell ' . round($free / 1048576) . ' MB)' : '') . '?'];
}
}
} else {
// Eigene ZIP-Implementierung (kein php_zip nötig)
$zip = new SimpleZip($backupPath);
@@ -213,11 +258,21 @@ Deny from all
return ['ok' => false, 'error' => 'Backup-Datei wurde nicht erstellt.'];
}
// Automatische Prüfung: Ein Backup, das sich nicht wiederherstellen lässt, ist wertlos. Das Ergebnis steht in der Liste und im Hinweis im System.
// (Backups ohne Datenbank, zum Beispiel reine Datei-Sicherungen von Plugins, lassen sich so nicht prüfen und werden hier ausgelassen.)
$verify = null;
if ($withDb && ($options['verify'] ?? true)) {
if (!function_exists('backup_verify_and_record')) require_once __DIR__ . '/backup_restore.php';
$verify = backup_verify_and_record($backupPath);
}
return [
'ok' => true,
'filename' => $backupName,
'path' => $backupPath,
'files' => count($files),
'skipped' => $skipped,
'verify' => $verify,
'size' => round(filesize($backupPath) / 1024 / 1024, 2),
];
}