Files
deskhub/Plugins/plugins/nas-backup/pages/einstellungen.php
T
2026-10-19 18:14:57 +00:00

233 lines
19 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
// Seite „Synology NAS Backup“ (Menüpunkt). Verfügbar: $staff, $pdo. Nur für Administratoren (Berechtigung "manage_nas_backup").
$message = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$do = (string) ($_POST['do'] ?? '');
$r = nasbk_page_action($do, $_POST, $staff);
audit_log($staff['id'], 'Synology NAS Backup: ' . $do . ' (' . ($r['ok'] ? 'erfolgreich' : 'fehlgeschlagen') . ')');
if ($r['ok']) $message = $r['message']; else $error = $r['message'];
}
$cfg = nasbk_config();
$hasPassword = $cfg['password'] !== '';
$running = nasbk_running();
$lastRun = (string) plugin_setting(NASBK, 'last_run', '');
$lastOk = plugin_setting(NASBK, 'last_ok', '') === '1';
$lastMsg = (string) plugin_setting(NASBK, 'last_message', '');
$lastOkAt = (string) plugin_setting(NASBK, 'last_ok_at', '');
$history = json_decode((string) plugin_setting(NASBK, 'history', '[]'), true);
$history = is_array($history) ? $history : [];
$remote = nasbk_config_error($cfg) === null ? nasbk_remote_cached($cfg) : null;
$fmtSize = fn(int $b) => $b >= 1048576 ? round($b / 1048576, 1) . ' MB' : max(1, round($b / 1024)) . ' KB';
$lastRunTs = $lastRun !== '' ? $lastRun : '';
$localDirReal = realpath(nasbk_local_dir());
$rows = [];
foreach (($localDirReal ? glob($localDirReal . '/backup_*.zip') : []) ?: [] as $p) {
if (!nasbk_valid_name(basename($p))) continue;
$rows[basename($p)] = ['name' => basename($p), 'size' => filesize($p), 'date' => filemtime($p), 'local' => true, 'remote' => null];
}
if ($remote !== null && $remote['ok']) {
foreach ($remote['items'] as $it) {
if (isset($rows[$it['name']])) { $rows[$it['name']]['remote'] = $it['size']; continue; }
$rows[$it['name']] = ['name' => $it['name'], 'size' => $it['size'], 'date' => strtotime($it['modified']) ?: 0, 'local' => false, 'remote' => $it['size']];
}
}
uasort($rows, fn($a, $b) => strcmp($b['name'], $a['name']));
$checkMap = json_decode((string) get_setting('backup_check_map', ''), true);
$checkMap = is_array($checkMap) ? $checkMap : [];
?>
<style>
.nb-card { margin-bottom:16px; }
.nb-card h3 { margin-top:0; padding-bottom:8px; border-bottom:1px solid var(--border); }
.nb-row { display:grid; grid-template-columns:200px minmax(0,1fr); gap:6px 18px; padding:11px 0; align-items:start; }
.nb-row > label.nb-l { font-weight:600; margin:8px 0 0; }
.nb-hint { font-size:.8rem; color:var(--muted); margin:4px 0 0; }
.nb-rule { background:#fffbeb; border:1px solid #fde68a; color:#92400e; border-radius:8px; padding:7px 11px; font-size:.85rem; margin-bottom:6px; }
.nb-radio { display:flex; align-items:flex-start; gap:8px; font-weight:400; margin:0 0 6px; }
.nb-radio input { width:auto; margin-top:3px; }
@media (max-width:760px) { .nb-row { grid-template-columns:1fr; } }
</style>
<?php if ($running): ?><script>setTimeout(function () { location.reload(); }, 10000);</script><?php endif; ?>
<h1>Synology NAS Backup</h1>
<p class="muted">Kopiert die Backups dieses Servers (Zeitplan und Knopf der Seite „Backup“) automatisch auf deine Synology NAS oder einen anderen WebDAV-Server und kann zusätzlich eigene Backups nach Zeitplan erstellen.</p>
<?php if ($message): ?><div class="flash success"><?= e($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="flash error"><?= e($error) ?></div><?php endif; ?>
<div class="card nb-card">
<h3>Status</h3>
<?php if ($running): ?>
<p style="margin:0"><strong style="color:#b45309">⏳ Ein Backup läuft gerade …</strong> <span class="muted" style="font-size:.85rem">Die Seite lädt sich von selbst neu.</span></p>
<?php elseif ($lastRun === ''): ?>
<p class="muted" style="margin:0">Noch kein Vorgang gelaufen.</p>
<?php else: ?>
<p style="margin:0"><strong style="color:<?= $lastOk ? '#15803d' : '#b91c1c' ?>"><?= $lastOk ? '✓ Letzter Vorgang erfolgreich' : '✗ Letzter Vorgang fehlgeschlagen' ?></strong> · <?= e($lastRun) ?></p>
<p class="muted" style="margin:4px 0 0;font-size:.88rem"><?= e($lastMsg) ?></p>
<?php endif; ?>
<?php if (!$cfg['enabled']): ?><p class="muted" style="margin:8px 0 0;font-size:.85rem">Das NAS-Backup ist ausgeschaltet.</p>
<?php elseif ($remote !== null && !$remote['ok']): ?><p style="margin:8px 0 0;font-size:.85rem;color:#b91c1c">✗ NAS nicht erreichbar: <?= e($remote['error']) ?></p>
<?php elseif ($remote !== null): ?><p class="muted" style="margin:8px 0 0;font-size:.85rem">✓ NAS erreichbar · <?= count($remote['items']) ?> Sicherung(en) dort<?= $cfg['mirror'] ? ' · neue Backups des Servers werden automatisch kopiert' : '' ?></p><?php endif; ?>
</div>
<div class="card nb-card">
<h3>Backups</h3>
<?php if (!$rows): ?>
<p class="muted" style="margin:0">Noch keine Backups. Sie entstehen auf der Seite „Backup“ (Zeitplan oder „Backup jetzt erstellen“) und werden dann hierher kopiert.</p>
<?php else: ?>
<table>
<thead><tr><th>Datei</th><th>Größe</th><th>Erstellt</th><th>Status</th><th></th></tr></thead>
<tbody>
<?php foreach ($rows as $b): $chk = $checkMap[$b['name']] ?? null; $onNas = $b['remote'] !== null; $same = $onNas && $b['local'] && $b['remote'] === $b['size']; ?>
<tr>
<td style="font-family:monospace;font-size:.82rem"><?= e($b['name']) ?></td>
<td class="muted"><?= e($fmtSize((int) $b['size'])) ?></td>
<td class="muted"><?= $b['date'] ? e(date('d.m.Y H:i', (int) $b['date'])) : '–' ?></td>
<td style="white-space:nowrap">
<?php if ($b['local']): ?><span class="muted" style="font-size:.78rem">auf dem Server</span> <?= $chk ? ($chk['ok'] ? '<span style="color:#15803d;font-size:.78rem">✔ geprüft</span>' : '<span style="color:#b91c1c;font-size:.78rem">✖ defekt</span>') : '<span class="muted" style="font-size:.78rem">nicht geprüft</span>' ?><br><?php endif; ?>
<?php if ($same): ?><span style="color:#1d4ed8;font-size:.78rem">☁ auf der NAS</span>
<?php elseif ($onNas && $b['local']): ?><span style="color:#b91c1c;font-size:.78rem">☁ unvollständig auf der NAS</span>
<?php elseif ($onNas): ?><span style="color:#1d4ed8;font-size:.78rem">☁ nur auf der NAS</span>
<?php elseif ($remote !== null && $remote['ok']): ?><span class="muted" style="font-size:.78rem">noch nicht auf der NAS</span><?php endif; ?>
</td>
<td><div style="display:flex;gap:5px;justify-content:flex-end">
<?php if ($b['local'] && !$same && $remote !== null && $remote['ok']) echo nasbk_action_form('upload', '☁ Auf NAS', ['file' => $b['name']]); ?>
<?php if (!$b['local']) echo nasbk_action_form('fetch', '⬇ Auf Server holen', ['file' => $b['name']], 'Diese Sicherung von der NAS auf den Server laden? Danach kannst du sie auf der Seite „Backup“ wiederherstellen.') . nasbk_action_form('delete_remote', '🗑', ['file' => $b['name']], 'Diese Sicherung von der NAS löschen?', 'secondary', 'border-color:var(--danger);color:var(--danger)'); ?>
</div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="nb-hint">Wiederherstellen und Löschen auf dem Server machst du auf der Seite „Backup“. Sicherungen, die nur auf der NAS liegen, holst du hier zuerst auf den Server.</p>
<?php endif; ?>
</div>
<form method="post" id="nb-form">
<?= csrf_field() ?>
<input type="hidden" name="do" value="save">
<div class="card nb-card">
<h3>🔌 Verbindung zur NAS</h3>
<label style="display:flex;align-items:center;gap:8px;margin:0 0 6px"><input type="checkbox" name="enabled" value="1" style="width:auto" <?= $cfg['enabled'] ? 'checked' : '' ?>> <strong>NAS-Backup verwenden</strong></label>
<div class="nb-row"><label class="nb-l" for="host">IP-Adresse oder Domain</label>
<div><input type="text" id="host" name="host" value="<?= e($cfg['host']) ?>" placeholder="nas.meinedomain.de" autocomplete="off">
<p class="nb-hint">Von außen erreichbar: Domain oder DynDNS, zum Beispiel <code>meine-nas.synology.me</code>. Dein Server läuft nicht in deinem Netz, die NAS muss also aus dem Internet erreichbar sein.</p></div></div>
<div class="nb-row"><label class="nb-l" for="port">Port</label>
<div><input type="number" id="port" name="port" value="<?= (int) $cfg['port'] ?>" min="1" max="65535" style="max-width:130px">
<p class="nb-hint">Standard Synology WebDAV: <strong>5005</strong> (HTTP) | <strong>5006</strong> (HTTPS). Über einen Reverse-Proxy: 443.</p></div></div>
<div class="nb-row"><label class="nb-l" for="user">Benutzername</label><div><input type="text" id="user" name="user" value="<?= e($cfg['user']) ?>" autocomplete="off"></div></div>
<div class="nb-row"><label class="nb-l" for="password">Passwort</label>
<div><input type="password" id="password" name="password" autocomplete="new-password" placeholder="<?= $hasPassword ? '•••••••• (gespeichert, leer lassen = behalten)' : '' ?>">
<p class="nb-hint">Am besten ein eigener NAS-Benutzer, der nur auf den Backup-Ordner zugreifen darf. Das Passwort wird verschlüsselt gespeichert.</p></div></div>
<div class="nb-row"><label class="nb-l">HTTPS (SSL)</label>
<div><label class="nb-radio" style="margin:0"><input type="checkbox" id="https" name="https" value="1" <?= $cfg['https'] ? 'checked' : '' ?>> Verschlüsselte Verbindung (HTTPS) verwenden</label>
<p class="nb-hint">Ohne HTTPS ist nur eine Adresse im eigenen Netz erlaubt, sonst würden Passwort und Backup unverschlüsselt durchs Internet gehen.</p></div></div>
<div class="nb-row"><label class="nb-l">Server-URL (Vorschau)</label><div><code id="nb-preview" style="display:inline-block;padding:6px 10px;border:1px solid var(--border);border-radius:6px">—</code></div></div>
<div class="nb-row"><label class="nb-l" for="target">Zielverzeichnis auf der NAS</label>
<div><input type="text" id="target" name="target" value="<?= e($cfg['target']) ?>" placeholder="/backup/DeskHub/">
<p class="nb-hint">Beginnt mit dem Namen des freigegebenen Ordners, zum Beispiel <code>/usbshare2/Geschichten/</code>. Unterordner legt das Plugin an, die Freigabe selbst muss es auf der NAS schon geben.</p></div></div>
<div class="nb-row"><label class="nb-l">Zertifikat</label>
<div><label class="nb-radio" style="margin:0"><input type="checkbox" name="verify_tls" value="1" <?= $cfg['verify_tls'] ? 'checked' : '' ?>> Zertifikat der NAS prüfen (empfohlen)</label>
<p class="nb-hint">Nur abschalten, wenn die NAS ein selbst erstelltes Zertifikat hat. Dann ist die Verbindung nicht mehr gegen Mithören und Fälschung geschützt. Besser: kostenloses Zertifikat von Let’s Encrypt in der NAS einrichten.</p></div></div>
</div>
<div class="card nb-card">
<h3>🔁 Zusammenarbeit mit der Backup-Seite</h3>
<label class="nb-radio"><input type="checkbox" name="mirror" value="1" <?= $cfg['mirror'] ? 'checked' : '' ?>>
<span><strong>Jedes Backup von DeskHub automatisch auf die NAS kopieren</strong><br><span class="nb-hint">Gilt für den Zeitplan der Seite „Backup“, für manuelle Backups und für Backups vor Updates. Die Kopie läuft nach dem Backup im Hintergrund und bremst es nicht. Auf der Seite „Backup“ siehst du bei jeder Sicherung, ob sie auf der NAS liegt, kannst Sicherungen auf die NAS legen und Sicherungen, die nur dort liegen, auf den Server holen und wiederherstellen.</span></span></label>
</div>
<div class="card nb-card">
<h3>📦 Backup-Inhalt <span class="muted" style="font-size:.8rem;font-weight:400">(für die eigenen Backups des Plugins)</span></h3>
<p class="nb-hint" style="margin:0 0 6px">Datenbank und Dateien werden in <strong>ein einziges ZIP-Archiv</strong> (<code>backup_DATUM_UHRZEIT.zip</code>) gepackt und auf die NAS hochgeladen. Das Archiv lässt sich auf der Seite „Backup“ wiederherstellen.</p>
<div class="nb-row"><label class="nb-l">Was soll gesichert werden?</label>
<div>
<label class="nb-radio"><input type="checkbox" name="db" value="1" <?= $cfg['with_database'] ? 'checked' : '' ?>><span><strong>Datenbank</strong> – alle Tabellen</span></label>
<label class="nb-radio"><input type="checkbox" name="files" value="1" <?= $cfg['with_files'] ? 'checked' : '' ?>><span><strong>Dateien</strong></span></label>
</div></div>
<div class="nb-row"><label class="nb-l">Datei-Umfang</label>
<div>
<label class="nb-radio"><input type="radio" name="scope" value="important" <?= $cfg['scope'] === 'important' ? 'checked' : '' ?>><span><strong>Wichtigste Dateien</strong> – <code>uploads/</code>, <code>plugins/</code>, <code>config/</code>, <code>assets/branding/</code>, <code>.htaccess</code></span></label>
<label class="nb-radio"><input type="radio" name="scope" value="full" <?= $cfg['scope'] === 'full' ? 'checked' : '' ?>><span><strong>Vollständige DeskHub-Installation</strong> (alle Programmdateien)</span></label>
</div></div>
<div class="nb-row"><label class="nb-l" for="excludes">Pfade ausschließen</label>
<div><textarea id="excludes" name="excludes" rows="4" placeholder="uploads/2020&#10;app_downloads"><?= e(implode("\n", $cfg['excludes'])) ?></textarea>
<p class="nb-hint">Ein Pfad pro Zeile, relativ zum DeskHub-Hauptordner. Beispiel: <code>uploads/2020</code> oder <code>app_downloads</code>.</p></div></div>
</div>
<div class="card nb-card">
<h3>🕐 Backup-Zeitplan <span class="muted" style="font-size:.8rem;font-weight:400">(optional, zusätzlich)</span></h3>
<label class="nb-radio"><input type="checkbox" name="own_schedule" value="1" <?= $cfg['own_schedule'] ? 'checked' : '' ?>>
<span><strong>Das Plugin erstellt zusätzlich eigene Backups nach diesem Zeitplan</strong><br><span class="nb-hint">Nur nötig, wenn du auf der Seite „Backup“ keinen Zeitplan nutzt oder die NAS häufiger sichern soll. Diese Backups liegen nur auf der NAS, nicht auf dem Server.</span></span></label>
<div class="nb-row"><label class="nb-l" for="interval">Intervall</label>
<div><select id="interval" name="interval" style="max-width:220px"><?php foreach (nasbk_intervals() as $k => [$label]): ?><option value="<?= e($k) ?>" <?= $cfg['interval'] === $k ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select></div></div>
<div class="nb-row"><label class="nb-l" for="time">Uhrzeit</label>
<div><input type="time" id="time" name="time" value="<?= e($cfg['time']) ?>" style="max-width:160px">
<p class="nb-hint">Gilt für tägliche, wöchentliche und monatliche Backups (Serverzeit). Bei stündlichen und mehrstündigen Intervallen wird die Uhrzeit ignoriert.<br>Der Zeitplan läuft mit, sobald jemand im Adminportal aktiv ist oder ein Cronjob DeskHub aufruft.</p></div></div>
</div>
<div class="card nb-card">
<h3>🗑 Aufbewahrung &amp; Bereinigung</h3>
<p class="nb-hint" style="margin:0 0 6px">Beide Regeln gelten gleichzeitig – ein Backup wird gelöscht, sobald <em>eine</em> davon zutrifft. Es werden nur Dateien gelöscht, die dieses Plugin selbst angelegt hat (<code>backup_….zip</code>), eigene Dateien im Ordner bleiben unberührt.</p>
<div class="nb-row"><label class="nb-l" for="keep_days">Maximales Alter</label>
<div><input type="number" id="keep_days" name="keep_days" min="0" max="3650" value="<?= (int) $cfg['keep_days'] ?>" style="max-width:110px"> Tage
<p class="nb-hint">Backups älter als diese Anzahl Tage werden gelöscht. <strong>0 = deaktiviert.</strong></p></div></div>
<div class="nb-row"><label class="nb-l" for="keep">Maximale Anzahl</label>
<div><input type="number" id="keep" name="keep" min="0" max="365" value="<?= (int) $cfg['keep'] ?>" style="max-width:110px"> Backups
<p class="nb-hint">Es werden maximal so viele <code>backup_*.zip</code>-Dateien auf der NAS behalten. Überschüssige werden – älteste zuerst – gelöscht. <strong>0 = deaktiviert.</strong></p></div></div>
<div class="nb-row"><label class="nb-l">Aktuell aktive Regeln</label><div id="nb-rules"></div></div>
</div>
<div class="btn-row"><button type="submit" class="btn">Einstellungen speichern</button></div>
</form>
<div class="card nb-card" style="margin-top:16px">
<h3>Ausprobieren</h3>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<form method="post"><?= csrf_field() ?><input type="hidden" name="do" value="test"><button type="submit" class="btn secondary">🔍 Verbindung testen</button></form>
<form method="post"><?= csrf_field() ?><input type="hidden" name="do" value="sync"><button type="submit" class="btn secondary">☁ Fehlende Backups jetzt kopieren</button></form>
<form method="post"><?= csrf_field() ?><input type="hidden" name="do" value="run"><button type="submit" class="btn" <?= $running ? 'disabled' : '' ?>>▶ Eigenes Backup jetzt starten</button></form>
</div>
<p class="nb-hint">Speichere zuerst. Der Verbindungstest legt den Ordner an, schreibt eine kleine Testdatei und löscht sie wieder. „Eigenes Backup jetzt starten“ erzeugt ein Backup mit den Einstellungen oben, lädt es hoch und läuft im Hintergrund. Die vorhandenen Backups des Servers verwaltest du auf der Seite „Backup“.</p>
</div>
<?php if ($history): ?>
<div class="card nb-card">
<h3>Verlauf</h3>
<?php foreach ($history as $h): ?>
<div style="font-size:.85rem;padding:4px 0;border-bottom:1px solid var(--border)">
<strong style="color:<?= !empty($h['ok']) ? '#15803d' : '#b91c1c' ?>"><?= !empty($h['ok']) ? '✓' : '✗' ?></strong>
<?= e((string) ($h['time'] ?? '')) ?><?= !empty($h['manual']) ? ' <span class="muted">(manuell)</span>' : '' ?> – <span class="muted"><?= e((string) ($h['message'] ?? '')) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<script>
(function () {
var $ = function (id) { return document.getElementById(id); };
function preview() {
var host = $('host').value.trim(), ssl = $('https').checked, port = $('port').value.trim();
var def = ssl ? '443' : '80';
$('nb-preview').textContent = host ? (ssl ? 'https' : 'http') + '://' + host + (port && port !== def ? ':' + port : '') + ($('target').value ? '/' + $('target').value.replace(/^\/+|\/+$/g, '') + '/' : '/') : '—';
}
function rules() {
var days = parseInt($('keep_days').value, 10) || 0, count = parseInt($('keep').value, 10) || 0, out = '';
if (days > 0) {
var d = new Date(Date.now() - days * 86400000);
out += '<div class="nb-rule">📅 Backups älter als ' + d.toLocaleDateString('de-DE') + ' (' + days + ' Tage) werden gelöscht</div>';
}
if (count > 0) out += '<div class="nb-rule">🔢 Es werden maximal ' + count + ' Backups auf der NAS behalten</div>';
if (!out) out = '<div class="nb-hint" style="margin:0">Keine Regel aktiv – Backups werden nie automatisch von der NAS gelöscht.</div>';
$('nb-rules').innerHTML = out;
}
['host', 'port', 'https', 'target'].forEach(function (id) { $(id).addEventListener('input', preview); $(id).addEventListener('change', preview); });
['keep_days', 'keep'].forEach(function (id) { $(id).addEventListener('input', rules); });
$('https').addEventListener('change', function () {
var p = $('port'); if (p.value === '80' && this.checked) p.value = '443'; else if (p.value === '443' && !this.checked) p.value = '80';
preview();
});
preview(); rules();
})();
</script>