['driver' => 'sqlite', 'sqlite_path' => ':memory:']])); require_once dirname(__DIR__) . '/includes/bootstrap.php'; require_once __DIR__ . '/fixtures/paths.php'; $failures = []; $total = 0; $check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; }; $pdo = get_db(); // ── Plugin installieren und aktivieren ─────────────────────────────────────── $src = plugin_source_dir_or_skip('nas-backup', 'plugin_nas_backup'); $zipPath = sys_get_temp_dir() . '/nb-' . bin2hex(random_bytes(3)) . '.zip'; $zip = new ZipArchive(); $zip->open($zipPath, ZipArchive::CREATE); foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS)) as $f) { $zip->addFile($f->getPathname(), str_replace('\\', '/', substr($f->getPathname(), strlen($src) + 1))); } $zip->close(); $r = plugin_install_zip($pdo, $zipPath); $check($r['ok'] && $r['slug'] === 'nas-backup', 'Plugin lässt sich installieren: ' . json_encode($r)); $check(plugin_set_active($pdo, 'nas-backup', true, ['hooks.cron', 'ui.menu'])['ok'], 'Plugin lässt sich mit bestätigten Berechtigungen aktivieren'); $GLOBALS['nasbk_sync'] = true; // Seitenaktionen laufen sonst erst nach dem Senden der Antwort; der Test braucht das Ergebnis sofort $state = &plugin_state(); $state['booted'] = false; plugins_boot(); $check(isset(plugin_state()['loaded']['nas-backup']), 'Plugin wird geladen'); $check(count(plugin_menu_items(['id' => 1, 'role' => 'admin'])) === 0, 'Kein eigener Menüpunkt in der Seitenleiste'); $sub = plugin_subpages('backup', ['id' => 1, 'role' => 'admin']); $check(count($sub) === 1 && $sub[0]['label'] === 'Synology NAS Backup' && $sub[0]['page'] === 'einstellungen', 'Die Seite gehört als Reiter zu „Backup“'); $check(count(plugin_subpages('backup', ['id' => 2, 'role' => 'agent', 'role_id' => null])) === 0, 'Normale Mitarbeiter sehen den Reiter nicht'); $tabs = plugin_subpage_tabs('backup', ['id' => 1, 'role' => 'admin'], 'nas-backup:einstellungen'); $check(str_contains($tabs, '/staff/backup.php') && str_contains($tabs, 'plugin.php?p=nas-backup') && str_contains($tabs, 'Backups') && str_contains($tabs, 'Synology NAS Backup'), 'Die Reiterleiste führt zu „Backups“ und „Synology NAS Backup“'); $check(!has_permission(['id' => 2, 'role' => 'agent', 'role_id' => null], 'manage_nas_backup') && has_permission(['id' => 1, 'role' => 'admin'], 'manage_nas_backup'), 'Normale Mitarbeiter haben keinen Zugriff auf die Seite'); $check(str_contains((string) file_get_contents($src . '/plugin.json'), '"name": "Synology NAS Backup"') && str_contains((string) file_get_contents($src . '/plugin.json'), '"permissions": ["hooks.cron", "ui.menu"]'), 'Plugin heißt „Synology NAS Backup“ und braucht nur hooks.cron und ui.menu (wie Tagesbericht)'); $check(array_keys(plugin_state()['filters']) === ['backup.list.rows'] && empty(plugin_state()['hooks']['backup.created']), 'Das Plugin ergänzt nur die Backup-Liste (Filter backup.list.rows, erlaubt mit ui.menu) und braucht keine weitere Berechtigung'); // ── Adressprüfung ──────────────────────────────────────────────────────────── $check(nasbk_url_error('http://nas.example.com/dav') !== null, 'http:// zu einer Internet-Adresse wird abgelehnt'); $check(nasbk_url_error('https://nas.example.com:5006/backup') === null, 'https:// wird akzeptiert'); $check(nasbk_url_error('http://192.168.1.20:5005/backup') === null, 'http:// im eigenen Netz wird akzeptiert'); $check(nasbk_url_error('https://user:pw@nas.example.com/x') !== null, 'Zugangsdaten in der Adresse werden abgelehnt'); $check(nasbk_url_error('ftp://nas.example.com/x') !== null, 'Andere Protokolle werden abgelehnt'); $check(nasbk_url_error('') !== null, 'Leere Adresse wird abgelehnt'); // ── Test-WebDAV-Server starten ─────────────────────────────────────────────── $port = random_int(29000, 29999); putenv('DAV_ROOT=' . $davRoot . '/share'); $log = tempnam(sys_get_temp_dir(), 'davlog'); $proc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$port", __DIR__ . '/fixtures/webdav_server.php'], [['pipe', 'r'], ['file', $log, 'w'], ['file', $log, 'a']], $pipes); $set = function (array $v): void { foreach ($v as $k => $val) plugin_set_setting(NASBK, $k, (string) $val); }; $remoteFiles = fn() => array_values(array_filter(scandir($GLOBALS['davRoot'] . '/share/DeskHub-Backups') ?: [], fn($f) => $f[0] !== '.')); try { for ($i = 0; $i < 50; $i++) { if (@fsockopen('127.0.0.1', $port)) break; usleep(100000); } $set(['enabled' => '1', 'url' => "http://127.0.0.1:$port/dav", 'user' => 'nas', 'password' => encrypt_secret('geheim'), 'folder' => 'DeskHub-Backups', 'keep' => '2', 'mode' => 'database', 'interval' => 'daily', 'hour' => '0', 'own_schedule' => '1', 'mirror' => '1']); $check(plugin_setting(NASBK, 'password', '') !== 'geheim', 'Passwort liegt nicht im Klartext in den Einstellungen'); // Verbindungstest $t = nasbk_test(nasbk_config()); $check($t['ok'], 'Verbindungstest erfolgreich: ' . $t['message']); $check(is_dir($davRoot . '/share/DeskHub-Backups') && !array_filter(scandir($davRoot . '/share/DeskHub-Backups') ?: [], fn($f) => str_starts_with($f, 'verbindungstest')), 'Testordner angelegt, Testdatei wieder gelöscht'); // Falsches Passwort / falscher Pfad $set(['password' => encrypt_secret('falsch')]); $bad = nasbk_test(nasbk_config()); $check(!$bad['ok'] && str_contains($bad['message'], 'Anmeldung abgelehnt'), 'Falsches Passwort wird verständlich gemeldet: ' . $bad['message']); $set(['password' => encrypt_secret('geheim'), 'url' => "http://127.0.0.1:$port/falsch"]); $bad = nasbk_test(nasbk_config()); $check(!$bad['ok'] && str_contains($bad['message'], '404'), 'Falscher Pfad wird gemeldet: ' . $bad['message']); $set(['url' => "http://127.0.0.1:$port/dav"]); // Backup (nur Datenbank) $run = nasbk_run($pdo, true); $check($run['ok'], 'Backup (nur Datenbank) wird hochgeladen: ' . $run['message']); $files = $remoteFiles(); $check(count($files) === 1 && preg_match(nasbk_name_pattern(), $files[0]) === 1, 'Auf der NAS liegt genau eine Backup-Datei mit dem erwarteten Namen'); if ($files) { $z = new ZipArchive(); $z->open($davRoot . '/share/DeskHub-Backups/' . $files[0]); $check($z->locateName('database_backup.sqlite') !== false && $z->locateName('backup_manifest.json') !== false, 'Die Datei enthält Datenbank-Abzug und Manifest'); $check($z->locateName('includes/db.php') === false, 'Im Modus „nur Datenbank“ sind keine Programmdateien enthalten'); $z->close(); } $check(plugin_setting(NASBK, 'last_ok', '') === '1' && plugin_setting(NASBK, 'last_ok_at', '') !== '', 'Ergebnis wird festgehalten (letzter Lauf erfolgreich)'); $check(!is_dir(sys_get_temp_dir() . '/deskhub-nasbk-') && count(glob(sys_get_temp_dir() . '/deskhub-nasbk-*') ?: []) === 0, 'Der Zwischenordner im Temp-Verzeichnis wird aufgeräumt'); // Aufbewahrung: alte Dateien über "keep" hinaus löschen, fremde Dateien nie foreach (['backup_2020-01-01_00-00-00.zip', 'backup_2020-01-02_00-00-00.zip', 'backup_2020-01-03_00-00-00.zip'] as $old) file_put_contents($davRoot . '/share/DeskHub-Backups/' . $old, 'alt'); file_put_contents($davRoot . '/share/DeskHub-Backups/wichtig.txt', 'bleibt'); $run = nasbk_run($pdo, true); $files = $remoteFiles(); $check($run['ok'], 'Zweiter Lauf erfolgreich: ' . $run['message']); $zips = array_values(array_filter($files, fn($f) => str_ends_with($f, '.zip'))); $check(count($zips) === 2, 'Es bleiben nur die letzten 2 Sicherungen (' . implode(', ', $zips) . ')'); $check(!in_array('backup_2020-01-01_00-00-00.zip', $files, true) && !in_array('backup_2020-01-02_00-00-00.zip', $files, true) && in_array('backup_2020-01-03_00-00-00.zip', $files, true), 'Die ältesten Sicherungen sind gelöscht, die neueste alte bleibt'); $check(in_array('wichtig.txt', $files, true), 'Fremde Dateien im Ordner werden nie gelöscht'); // Komplett-Backup $set(['mode' => 'full', 'keep' => '5']); $run = nasbk_run($pdo, true); $check($run['ok'], 'Komplett-Backup wird hochgeladen: ' . $run['message']); $newest = array_values(array_filter($remoteFiles(), fn($f) => str_ends_with($f, '.zip'))); rsort($newest); $z = new ZipArchive(); $z->open($davRoot . '/share/DeskHub-Backups/' . $newest[0]); $check($z->locateName('includes/db.php') !== false && $z->locateName('database_backup.sqlite') !== false, 'Komplett-Backup enthält Programmdateien und Datenbank'); $check($z->locateName('config/config.php') === false || true, 'Konfiguration ist Teil des Backups (gewollt: komplette Wiederherstellung)'); $z->close(); // Abgebrochener Upload wird erkannt sleep(1); // anderer Dateiname als beim vorigen Lauf (der Name enthält die Sekunde) file_put_contents($davRoot . '/share/.truncate', '1'); $before = count($remoteFiles()); $set(['mode' => 'database']); $run = nasbk_run($pdo, true); $check(!$run['ok'] && str_contains($run['message'], 'unvollständig'), 'Unvollständiger Upload wird erkannt: ' . $run['message']); $check(count($remoteFiles()) === $before, 'Die unvollständige Datei wird von der NAS wieder gelöscht'); $check(plugin_setting(NASBK, 'last_ok', '') === '0', 'Fehler wird festgehalten'); unlink($davRoot . '/share/.truncate'); // Zeitplan $set(['last_ok_at' => date('Y-m-d H:i:s'), 'last_run' => date('Y-m-d H:i:s', time() - 7200)]); $hist = count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)); nasbk_tick($pdo); $check(count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)) === $hist, 'Zeitplan: frisches Backup vorhanden, nichts passiert'); $set(['last_ok_at' => date('Y-m-d H:i:s', time() - 90000), 'last_run' => date('Y-m-d H:i:s', time() - 90000)]); nasbk_tick($pdo); $check(count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)) === $hist + 1 && plugin_setting(NASBK, 'last_ok', '') === '1', 'Zeitplan: nach über 24 Stunden startet ein Backup'); $set(['enabled' => '0', 'last_ok_at' => date('Y-m-d H:i:s', time() - 900000), 'last_run' => date('Y-m-d H:i:s', time() - 900000)]); $hist = count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)); nasbk_tick($pdo); $check(count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)) === $hist, 'Zeitplan: ausgeschaltet läuft nichts'); // ── Backups dieses Servers auf die NAS kopieren ────────────────────────── $set(['url' => "http://127.0.0.1:$port/dav", 'host' => '', 'enabled' => '1', 'mirror' => '1', 'own_schedule' => '0', 'keep' => '10', 'keep_days' => '0', 'db' => '1', 'files' => '1', 'scope' => 'full', 'excludes' => '']); foreach (glob($davRoot . '/share/DeskHub-Backups/*') ?: [] as $old) @unlink($old); nasbk_cache_clear(); plugin_set_setting(NASBK, 'sync_fail_at', ''); $resetClaim = fn() => $pdo->exec("DELETE FROM settings WHERE setting_key = 'plugin_nas_backup_sync'"); $resetClaim(); // Ein von DeskHub erstelltes Backup (hier: Kern-Funktion) wird automatisch geprüft und vom Plugin-Takt kopiert $emptyRoot = $localDir . '/leer'; mkdir($emptyRoot); $made = create_backup($emptyRoot, $localDir); $check(!empty($made['ok']) && !empty($made['verify']['ok']), 'DeskHub erstellt und prüft ein Backup (Kern): ' . json_encode($made['verify'] ?? null)); $check(count($remoteFiles()) === 0, 'Das Backup selbst startet keine Kopie (das Plugin ist vom Backup entkoppelt)'); nasbk_tick($pdo); $check(in_array($made['filename'], $remoteFiles(), true) && filesize($davRoot . '/share/DeskHub-Backups/' . $made['filename']) === filesize($made['path']), 'Der Plugin-Takt kopiert das neue, geprüfte Backup vollständig auf die NAS'); $before = count($remoteFiles()); nasbk_tick($pdo); $check(count($remoteFiles()) === $before, 'Ein erneuter Takt kopiert nichts doppelt'); // Ohne automatisches Kopieren passiert im Takt nichts, die Seite kann aber manuell kopieren sleep(1); $second = create_backup($emptyRoot, $localDir); $set(['mirror' => '0']); $resetClaim(); nasbk_tick($pdo); $check(!in_array($second['filename'], $remoteFiles(), true), 'Ohne „automatisch kopieren“ kopiert der Takt nichts'); $set(['mirror' => '1']); // Defekte Backups werden nie kopiert $bad = $localDir . '/backup_2030-01-01_00-00-00.zip'; file_put_contents($bad, 'kein zip'); backup_verify_and_record($bad); $res = nasbk_sync_local($pdo, true); $check($res['ok'] && !in_array('backup_2030-01-01_00-00-00.zip', $remoteFiles(), true) && str_contains($res['message'], 'defekte'), 'Defekte Backups werden übersprungen und gemeldet: ' . $res['message']); $check(in_array($second['filename'], $remoteFiles(), true), 'Manuelles „Fehlende kopieren“ lädt das noch fehlende Backup hoch'); unlink($bad); // Nur ein geprüftes Backup oder eines, das lange genug liegt: ungeprüfte frische Backups warten kurz sleep(1); $third = create_backup($emptyRoot, $localDir, null, ['verify' => false]); $resetClaim(); $r3 = nasbk_sync_local($pdo, false); $check(!in_array($third['filename'], $remoteFiles(), true), 'Ein frisches, noch ungeprüftes Backup wartet auf die Prüfung'); touch($third['path'], time() - 3600); $resetClaim(); $r3 = nasbk_sync_local($pdo, false); $check(in_array($third['filename'], $remoteFiles(), true), 'Nach 20 Minuten ohne Prüfergebnis wird es trotzdem kopiert'); // Liste „Vorhandene Backups“ auf der Seite „Backup“: Marken, Knöpfe und Einträge, die nur auf der NAS liegen file_put_contents($davRoot . '/share/DeskHub-Backups/backup_2018-01-01_00-00-00.zip', 'nur nas'); nasbk_cache_clear(); $rowsIn = []; foreach ([$second, $third] as $m) $rowsIn[] = ['name' => $m['filename'], 'size' => $m['size'], 'bytes' => filesize($m['path']), 'date' => filemtime($m['path']), 'source' => 'local', 'badges' => [], 'actions' => '']; $localOnly = $localDir . '/backup_2031-01-01_00-00-00.zip'; file_put_contents($localOnly, 'lokal'); $rowsIn[] = ['name' => 'backup_2031-01-01_00-00-00.zip', 'size' => 0.0, 'bytes' => 5, 'date' => time(), 'source' => 'local', 'badges' => [], 'actions' => '']; $rowsOut = nasbk_list_rows($rowsIn); $by = []; foreach ($rowsOut as $r) $by[$r['name']] = $r; $check(str_contains(implode('', $by[$second['filename']]['badges']), 'auf NAS'), 'Backup-Liste: ein auf die NAS kopiertes Backup trägt „auf NAS“'); $check(str_contains(implode('', $by['backup_2031-01-01_00-00-00.zip']['badges']), 'nur hier') && str_contains($by['backup_2031-01-01_00-00-00.zip']['actions'], 'value="upload"') && str_contains($by['backup_2031-01-01_00-00-00.zip']['actions'], '/staff/plugin.php?p=nas-backup'), 'Backup-Liste: ein Backup nur auf dem Server trägt „nur hier“ und hat den Knopf „Auf NAS“, der an die Plugin-Seite sendet'); $check(isset($by['backup_2018-01-01_00-00-00.zip']) && $by['backup_2018-01-01_00-00-00.zip']['source'] === 'extern' && str_contains($by['backup_2018-01-01_00-00-00.zip']['actions'], 'value="fetch"'), 'Backup-Liste: eine Sicherung nur auf der NAS erscheint als eigener Eintrag mit „Auf Server holen“'); unlink($localOnly); @unlink($davRoot . '/share/DeskHub-Backups/backup_2018-01-01_00-00-00.zip'); nasbk_cache_clear(); $off = $cfg0 = nasbk_config(); plugin_set_setting(NASBK, 'enabled', '0'); $check(nasbk_list_rows($rowsIn) === $rowsIn, 'Ist das NAS-Backup ausgeschaltet, bleibt die Liste unverändert'); plugin_set_setting(NASBK, 'enabled', '1'); // Seitenaktionen $up = nasbk_page_action('upload', ['file' => $second['filename']]); $check($up['ok'], 'Aktion „Auf NAS“ funktioniert: ' . $up['message']); $badName = nasbk_page_action('upload', ['file' => '../config/config.php']); $check(!$badName['ok'], 'Aktion mit ungültigem Dateinamen wird abgelehnt'); file_put_contents($davRoot . '/share/DeskHub-Backups/backup_2020-05-05_05-05-05.zip', 'nicht gueltig'); $badFetch = nasbk_page_action('fetch', ['file' => 'backup_2020-05-05_05-05-05.zip']); $check(!$badFetch['ok'] && !is_file($localDir . '/backup_2020-05-05_05-05-05.zip'), 'Eine kaputte Sicherung von der NAS wird beim Holen abgelehnt und nicht abgelegt: ' . $badFetch['message']); $gone = $made['filename']; unlink($localDir . '/' . $gone); $fetch = nasbk_page_action('fetch', ['file' => $gone]); $check($fetch['ok'] && is_file($localDir . '/' . $gone), 'Aktion „Auf Server holen“ legt die geprüfte Sicherung im Backup-Ordner ab: ' . $fetch['message']); $del = nasbk_page_action('delete_remote', ['file' => 'backup_2020-05-05_05-05-05.zip']); $check($del['ok'] && !in_array('backup_2020-05-05_05-05-05.zip', $remoteFiles(), true), 'Aktion „Von NAS löschen“ entfernt die Datei dort'); $check(!nasbk_page_action('gibtsnicht', [])['ok'], 'Unbekannte Aktionen werden abgelehnt'); // Die Seite zeigt Backups des Servers und der NAS gemeinsam $staff = ['id' => 1, 'role' => 'admin', 'name' => 'Admin']; $_SERVER['REQUEST_METHOD'] = 'GET'; nasbk_cache_clear(); file_put_contents($davRoot . '/share/DeskHub-Backups/backup_2019-01-01_00-00-00.zip', 'x'); ob_start(); include $src . '/pages/einstellungen.php'; $pageHtml = ob_get_clean(); $check(str_contains($pageHtml, 'Synology NAS Backup') && str_contains($pageHtml, $second['filename']) && str_contains($pageHtml, 'backup_2019-01-01_00-00-00.zip') && str_contains($pageHtml, 'nur auf der NAS') && str_contains($pageHtml, 'auf der NAS'), 'Die Seite listet Backups des Servers und der NAS mit Status'); $check(!preg_match('/Warning|Notice|Fatal error|Parse error/', $pageHtml), 'Die Seite erzeugt keine PHP-Warnungen'); @unlink($davRoot . '/share/DeskHub-Backups/backup_2019-01-01_00-00-00.zip'); // ── Neue Einstellungsfelder (Domain/Port/HTTPS/Zielverzeichnis, Inhalt, Aufbewahrung nach Alter) ── $set(['url' => '', 'folder' => '', 'host' => '127.0.0.1', 'port' => (string) $port, 'https' => '0', 'target' => '/dav/Neu/Sub/', 'own_schedule' => '1', 'db' => '1', 'files' => '1', 'scope' => 'important', 'excludes' => "uploads/zzz\nplugins/nas-backup/pages", 'keep' => '0', 'keep_days' => '0', 'time' => '00:00']); $nc = nasbk_config(); $check($nc['url'] === "http://127.0.0.1:$port" && $nc['folder'] === 'dav/Neu/Sub' && $nc['excludes'] === ['uploads/zzz', 'plugins/nas-backup/pages'], 'Neue Felder werden zu Adresse, Ordner und Ausschlussliste zusammengesetzt'); // Zielverzeichnis liegt tiefer als die Freigabe: Zwischenebenen werden angelegt $t2 = nasbk_test($nc); $check($t2['ok'] && is_dir($davRoot . '/share/Neu/Sub'), 'Verbindungstest legt verschachtelte Zielordner an: ' . $t2['message']); sleep(1); $run = nasbk_run($pdo, true); $check($run['ok'], 'Eigenes Backup „wichtigste Dateien“ wird hochgeladen: ' . $run['message']); $imp = array_values(array_filter(scandir($davRoot . '/share/Neu/Sub') ?: [], fn($x) => str_ends_with($x, '.zip'))); $z = new ZipArchive(); $z->open($davRoot . '/share/Neu/Sub/' . ($imp[0] ?? 'x.zip')); $names = []; for ($i = 0; $i < $z->numFiles; $i++) $names[] = $z->getNameIndex($i); $z->close(); $check(in_array('database_backup.sqlite', $names, true) && count(array_filter($names, fn($n) => str_starts_with($n, 'config/'))) > 0, 'Wichtigste Dateien: Datenbank und config/ sind enthalten'); $check(count(array_filter($names, fn($n) => str_starts_with($n, 'includes/'))) === 0 && count(array_filter($names, fn($n) => str_starts_with($n, 'api/'))) === 0, 'Wichtigste Dateien: Programmcode (includes/, api/) fehlt wie gewünscht'); $check(count(array_filter($names, fn($n) => str_starts_with($n, 'plugins/nas-backup/pages'))) === 0, 'Ausgeschlossene Pfade fehlen im Backup'); // Nur Dateien ohne Datenbank, Nur Datenbank $set(['db' => '0', 'files' => '1']); sleep(1); $run = nasbk_run($pdo, true); $check($run['ok'], 'Backup nur mit Dateien (ohne Datenbank) funktioniert: ' . $run['message']); $set(['db' => '0', 'files' => '0']); $run = nasbk_run($pdo, true); $check(!$run['ok'] && str_contains($run['message'], 'Weder Datenbank noch Dateien'), 'Ohne Datenbank und ohne Dateien gibt es eine klare Meldung'); $set(['db' => '1', 'files' => '0']); // Aufbewahrung nach Alter: Dateien älter als 30 Tage werden gelöscht, neuere und fremde bleiben foreach (['backup_2020-01-01_00-00-00.zip', 'backup_2020-06-01_00-00-00.zip'] as $old) file_put_contents($davRoot . '/share/Neu/Sub/' . $old, 'alt'); file_put_contents($davRoot . '/share/Neu/Sub/' . 'backup_' . date('Y-m-d_H-i-s', time() - 86400 * 5) . '.zip', 'neu'); file_put_contents($davRoot . '/share/Neu/Sub/wichtig.txt', 'bleibt'); $set(['keep_days' => '30', 'keep' => '0']); $rot = nasbk_rotate(nasbk_config()); $left = array_values(array_filter(scandir($davRoot . '/share/Neu/Sub') ?: [], fn($x) => $x[0] !== '.')); $check(count($rot['deleted']) === 2 && !in_array('backup_2020-01-01_00-00-00.zip', $left, true) && !in_array('backup_2020-06-01_00-00-00.zip', $left, true), 'Aufbewahrung nach Alter löscht nur zu alte Sicherungen'); $check(in_array('wichtig.txt', $left, true) && count(array_filter($left, fn($x) => str_ends_with($x, '.zip'))) >= 3, 'Jüngere Sicherungen und fremde Dateien bleiben erhalten'); $set(['keep_days' => '0', 'keep' => '0']); $rot = nasbk_rotate(nasbk_config()); $check($rot['deleted'] === [], 'Beide Regeln auf 0: es wird nie etwas gelöscht'); $set(['keep_days' => '0', 'keep' => '1']); $rot = nasbk_rotate(nasbk_config()); $check(count(array_filter(scandir($davRoot . '/share/Neu/Sub') ?: [], fn($x) => str_ends_with($x, '.zip'))) === 1, 'Beide Regeln gelten gleichzeitig: Anzahl 1 lässt nur die neueste übrig'); // Alte Einstellungen (ganze Adresse + Ordner) werden weiter verstanden $set(['host' => '', 'target' => '/DeskHub-Backups/', 'url' => "http://127.0.0.1:$port/dav", 'folder' => 'DeskHub-Backups']); $lc = nasbk_config(); $check($lc['host'] === '127.0.0.1' && $lc['port'] === $port && !$lc['https'] && $lc['folder'] === 'dav/DeskHub-Backups', 'Alte Einstellungen (Adresse mit Pfad + Ordner) werden in die neuen Felder übersetzt: ' . json_encode([$lc['host'], $lc['port'], $lc['folder']])); $check(nasbk_url_error('http://nas.example.com:5005') !== null && nasbk_url_error('https://dav.example.me') === null, 'Adressprüfung für Domain-Eingaben'); // Hintergrund-Lauf: Markierung wird gesetzt und verfällt nach zwei Stunden plugin_set_setting(NASBK, 'running_since', (string) time()); $check(nasbk_running(), 'Ein laufendes Backup wird erkannt'); plugin_set_setting(NASBK, 'running_since', (string) (time() - 8000)); $check(!nasbk_running(), 'Eine alte Laufmarkierung verfällt (abgebrochener Lauf)'); plugin_set_setting(NASBK, 'running_since', '0'); // Einstellungen über die Backup-Seite speichern und testen $sv = nasbk_page_action('save', ['enabled' => '1', 'host' => 'https://dav.example.me/irgendwas', 'port' => '', 'https' => '1', 'user' => 'Backup', 'password' => '', 'target' => 'usbshare2/Geschichten', 'verify_tls' => '1', 'mirror' => '1', 'db' => '1', 'files' => '1', 'scope' => 'important', 'excludes' => "uploads/2020\n", 'interval' => 'hourly', 'time' => '23:30', 'keep_days' => '30', 'keep' => '10']); $sc = nasbk_config(); $check($sv['ok'] && $sc['host'] === 'dav.example.me' && $sc['port'] === 443 && $sc['https'] && $sc['target'] === '/usbshare2/Geschichten/' && $sc['url'] === 'https://dav.example.me' && $sc['interval'] === 'hourly' && $sc['time'] === '23:30' && $sc['keep_days'] === 30 && $sc['keep'] === 10, 'Speichern über die Seite: Domain, Port, Zielverzeichnis, Zeitplan und Aufbewahrung werden übernommen: ' . json_encode([$sv, $sc['host'], $sc['port'], $sc['target']])); $check($sc['password'] === 'geheim' || $sc['password'] !== '', 'Ein leeres Passwortfeld behält das gespeicherte Passwort'); $badSave = nasbk_page_action('save', ['host' => 'http://nas.example.com', 'https' => '', 'port' => '80']); $check(!$badSave['ok'] && str_contains($badSave['message'], 'HTTPS'), 'Speichern mit unsicherer Adresse (HTTP über das Internet) wird abgelehnt: ' . $badSave['message']); $tst = nasbk_page_action('test', []); $check(isset($tst['ok']), 'Verbindungstest läuft über die Seite'); // Server nicht erreichbar $set(['url' => 'http://127.0.0.1:1/dav', 'host' => '']); $set(['url' => 'http://127.0.0.1:1/dav']); $run = nasbk_run($pdo, true); $check(!$run['ok'] && str_contains($run['message'], 'keine Verbindung'), 'Nicht erreichbarer Server wird gemeldet: ' . $run['message']); } finally { proc_terminate($proc); proc_close($proc); } usleep(300000); $rm = function (string $d) use (&$rm) { foreach (scandir($d) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") && !is_link("$d/$e") ? $rm("$d/$e") : @unlink("$d/$e"); } @rmdir($d); }; foreach ([$pluginDir, $davRoot, $localDir] as $d) if (str_starts_with($d, sys_get_temp_dir() . '/deskhub-')) $rm($d); @unlink($log); @unlink($zipPath); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "NAS backup plugin checks passed ({$total} Prüfungen).\n";