Upload via GUI (15 Dateien)
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,729 @@
|
||||
<?php
|
||||
/**
|
||||
* Logik des NAS-Backup-Plugins (ohne Seiten-/Hook-Code, damit sie einzeln testbar ist).
|
||||
*
|
||||
* Ablauf: Backup mit dem Kern erzeugen (create_backup: Dateien + Datenbank-Abzug), prüfen, per WebDAV (PUT) hochladen,
|
||||
* Größe auf dem Server kontrollieren, alte Sicherungen über die gewünschte Anzahl hinaus löschen, Ergebnis festhalten.
|
||||
*/
|
||||
|
||||
const NASBK = 'nas-backup';
|
||||
|
||||
/** Zeitpläne: Schlüssel => [Bezeichnung, Mindestabstand in Sekunden]. */
|
||||
function nasbk_intervals(): array
|
||||
{
|
||||
return [
|
||||
'hourly' => ['Stündlich', 3600],
|
||||
'6h' => ['Alle 6 Stunden', 21600],
|
||||
'12h' => ['Alle 12 Stunden', 43200],
|
||||
'daily' => ['Täglich', 86400],
|
||||
'weekly' => ['Wöchentlich', 604800],
|
||||
'monthly' => ['Monatlich', 2592000],
|
||||
];
|
||||
}
|
||||
/** Alle Einstellungen des Plugins in einem Array (mit Werten für Altbestände). */
|
||||
function nasbk_config(): array
|
||||
{
|
||||
$pass = (string) plugin_setting(NASBK, 'password', '');
|
||||
$interval = (string) plugin_setting(NASBK, 'interval', 'daily');
|
||||
|
||||
// Verbindung: Domain/IP, Port, HTTPS und Zielverzeichnis. Ältere Einstellungen (ganze Adresse + Ordner) werden weiter verstanden.
|
||||
$host = trim((string) plugin_setting(NASBK, 'host', ''));
|
||||
$port = (int) plugin_setting(NASBK, 'port', '0');
|
||||
$https = plugin_setting(NASBK, 'https', '1') !== '0';
|
||||
$targetRaw = plugin_setting(NASBK, 'target', null); // Standard unten setzen (Standardwerte von plugin_setting() werden zwischengespeichert)
|
||||
$target = (string) ($targetRaw ?? '');
|
||||
if ($host === '' && ($legacy = trim((string) plugin_setting(NASBK, 'url', ''))) !== '') {
|
||||
$p = parse_url($legacy);
|
||||
if ($p && !empty($p['host'])) {
|
||||
$host = $p['host'];
|
||||
$https = strtolower($p['scheme'] ?? 'https') !== 'http';
|
||||
$port = (int) ($p['port'] ?? 0);
|
||||
$target = trim((string) ($p['path'] ?? ''), '/') . '/' . trim((string) plugin_setting(NASBK, 'folder', 'DeskHub-Backups'), '/');
|
||||
}
|
||||
}
|
||||
if ($port <= 0) $port = $https ? 443 : 80;
|
||||
if ($target === '' && $targetRaw === null) $target = '/DeskHub-Backups/';
|
||||
$default = ($https && $port === 443) || (!$https && $port === 80);
|
||||
$url = $host !== '' ? ($https ? 'https' : 'http') . '://' . $host . ($default ? '' : ':' . $port) : '';
|
||||
|
||||
// Inhalt des eigenen Backups (Altbestand: Umfang "full"/"database")
|
||||
$legacyMode = plugin_setting(NASBK, 'mode', 'full') === 'database' ? 'database' : 'full';
|
||||
$db = plugin_setting(NASBK, 'db', '1') === '1';
|
||||
// Standardwerte hier berechnen, nicht über den Standardwert von plugin_setting() (dieser wird zwischengespeichert)
|
||||
$filesRaw = plugin_setting(NASBK, 'files', null);
|
||||
$files = ($filesRaw ?? ($legacyMode === 'database' ? '0' : '1')) === '1';
|
||||
$scopeRaw = plugin_setting(NASBK, 'scope', null);
|
||||
$scope = ($scopeRaw ?? ($legacyMode === 'database' ? 'important' : 'full')) === 'important' ? 'important' : 'full';
|
||||
|
||||
// Zeit: HH:MM (Altbestand: nur Stunde)
|
||||
$time = (string) plugin_setting(NASBK, 'time', '');
|
||||
if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $time)) $time = sprintf('%02d:00', max(0, min(23, (int) plugin_setting(NASBK, 'hour', '2'))));
|
||||
|
||||
return [
|
||||
'enabled' => plugin_setting(NASBK, 'enabled', '0') === '1',
|
||||
'mirror' => plugin_setting(NASBK, 'mirror', '1') === '1', // jedes von DeskHub erstellte Backup auf die NAS kopieren
|
||||
'own_schedule' => plugin_setting(NASBK, 'own_schedule', '0') === '1', // zusätzlich eigener Zeitplan des Plugins
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'https' => $https,
|
||||
'target' => $target,
|
||||
'url' => $url,
|
||||
'user' => (string) plugin_setting(NASBK, 'user', ''),
|
||||
'password' => $pass !== '' ? decrypt_secret($pass) : '',
|
||||
'folder' => trim(trim($target), '/'),
|
||||
'interval' => isset(nasbk_intervals()[$interval]) ? $interval : 'daily',
|
||||
'time' => $time,
|
||||
'hour' => (int) substr($time, 0, 2),
|
||||
'keep' => max(0, min(365, (int) plugin_setting(NASBK, 'keep', '14'))), // 0 = Anzahl nicht begrenzen
|
||||
'keep_days' => max(0, min(3650, (int) plugin_setting(NASBK, 'keep_days', '0'))), // 0 = Alter nicht begrenzen
|
||||
'with_database' => $db,
|
||||
'with_files' => $files,
|
||||
'scope' => $scope,
|
||||
'excludes' => array_values(array_filter(array_map('trim', preg_split('/\r?\n/', (string) plugin_setting(NASBK, 'excludes', '')) ?: []))),
|
||||
'mode' => $files ? 'full' : 'database',
|
||||
'verify_tls' => plugin_setting(NASBK, 'verify_tls', '1') !== '0',
|
||||
];
|
||||
}
|
||||
/** Prüft die Adresse: https ist Pflicht, http nur für Geräte im eigenen Netz (private oder lokale Adresse). Gibt eine Fehlermeldung oder null zurück. */
|
||||
/** Prüft die Adresse: https ist Pflicht, http nur für Geräte im eigenen Netz (private oder lokale Adresse). Gibt eine Fehlermeldung oder null zurück. */
|
||||
function nasbk_url_error(string $url): ?string
|
||||
{
|
||||
if ($url === '') return 'Bitte die Adresse (Domain oder IP) des WebDAV-Servers eintragen.';
|
||||
$p = parse_url($url);
|
||||
if ($p === false || empty($p['host']) || empty($p['scheme'])) return 'Die Adresse ist ungültig (Beispiel: nas.meinedomain.de).';
|
||||
if (isset($p['user']) || isset($p['pass']) || isset($p['query']) || isset($p['fragment'])) return 'Die Adresse darf keine Zugangsdaten, Parameter oder Anker enthalten. Benutzer und Passwort kommen in die eigenen Felder.';
|
||||
$scheme = strtolower($p['scheme']);
|
||||
if (!in_array($scheme, ['http', 'https'], true)) return 'Es sind nur Adressen mit https:// (oder im eigenen Netz http://) erlaubt.';
|
||||
if (!preg_match('/^[A-Za-z0-9.\-]+$|^\[[0-9a-f:]+\]$/i', $p['host'])) return 'Die Adresse enthält ungültige Zeichen.';
|
||||
if ($scheme === 'http') {
|
||||
$host = trim($p['host'], '[]');
|
||||
$local = $host === 'localhost' || (filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
|
||||
if (!$local) return 'Über das Internet ist nur HTTPS erlaubt, sonst würden Passwort und Backup unverschlüsselt übertragen. HTTP geht nur mit einer Adresse im eigenen Netz.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/** Fehler der gesamten Einstellung oder null, wenn alles Nötige da ist. */
|
||||
function nasbk_config_error(array $cfg): ?string
|
||||
{
|
||||
if ($e = nasbk_url_error($cfg['url'])) return $e;
|
||||
if ($cfg['user'] === '' || $cfg['password'] === '') return 'Bitte Benutzer und Passwort des NAS eintragen.';
|
||||
foreach (explode('/', $cfg['folder']) as $seg) {
|
||||
if ($seg === '.' || $seg === '..') return 'Der Ordnername ist ungültig.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Adresse eines Ordners/einer Datei auf dem Server (Ordnersegmente werden einzeln kodiert). */
|
||||
function nasbk_remote_url(array $cfg, string $file = '', bool $withFolder = true): string
|
||||
{
|
||||
$url = $cfg['url'];
|
||||
if ($withFolder && $cfg['folder'] !== '') {
|
||||
$url .= '/' . implode('/', array_map('rawurlencode', explode('/', $cfg['folder'])));
|
||||
}
|
||||
return $file !== '' ? $url . '/' . rawurlencode($file) : $url;
|
||||
}
|
||||
|
||||
/** Ein WebDAV-Aufruf. Optionen: body (Text), file (Pfad zum Hochladen), headers (Liste), timeout (Sekunden). */
|
||||
function nasbk_request(array $cfg, string $method, string $url, array $opt = []): array
|
||||
{
|
||||
if (!function_exists('curl_init')) return ['code' => 0, 'body' => '', 'error' => 'Auf diesem Server fehlt die PHP-Erweiterung cURL.'];
|
||||
$ch = curl_init($url);
|
||||
$headers = $opt['headers'] ?? [];
|
||||
$fh = null;
|
||||
$set = [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERPWD => $cfg['user'] . ':' . $cfg['password'],
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_FOLLOWLOCATION => false, // niemals Zugangsdaten an eine Weiterleitung schicken
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_TIMEOUT => (int) ($opt['timeout'] ?? 60),
|
||||
CURLOPT_SSL_VERIFYPEER => $cfg['verify_tls'],
|
||||
CURLOPT_SSL_VERIFYHOST => $cfg['verify_tls'] ? 2 : 0,
|
||||
CURLOPT_USERAGENT => 'DeskHub-NAS-Backup',
|
||||
];
|
||||
$out = null;
|
||||
if (isset($opt['save_to'])) {
|
||||
$out = fopen($opt['save_to'], 'wb');
|
||||
if ($out === false) return ['code' => 0, 'body' => '', 'error' => 'Die Zieldatei konnte nicht angelegt werden.'];
|
||||
$set[CURLOPT_RETURNTRANSFER] = false;
|
||||
$set[CURLOPT_FILE] = $out;
|
||||
}
|
||||
if (isset($opt['file'])) {
|
||||
$fh = fopen($opt['file'], 'rb');
|
||||
if ($fh === false) return ['code' => 0, 'body' => '', 'error' => 'Die Backup-Datei konnte nicht gelesen werden.'];
|
||||
$set[CURLOPT_UPLOAD] = true;
|
||||
$set[CURLOPT_INFILE] = $fh;
|
||||
$set[CURLOPT_INFILESIZE] = filesize($opt['file']);
|
||||
$headers[] = 'Expect:';
|
||||
} elseif (isset($opt['body'])) {
|
||||
$set[CURLOPT_POSTFIELDS] = $opt['body'];
|
||||
}
|
||||
if ($method === 'HEAD') { $set[CURLOPT_NOBODY] = true; $set[CURLOPT_HEADER] = true; }
|
||||
if ($headers) $set[CURLOPT_HTTPHEADER] = $headers;
|
||||
curl_setopt_array($ch, $set);
|
||||
$body = curl_exec($ch);
|
||||
$res = ['code' => (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE), 'body' => is_string($body) ? $body : '', 'error' => $body === false ? curl_error($ch) : ''];
|
||||
curl_close($ch);
|
||||
if ($fh) fclose($fh);
|
||||
if ($out) fclose($out);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/** Verständliche Fehlermeldung zu einer Antwort. */
|
||||
function nasbk_explain(array $r, string $what): string
|
||||
{
|
||||
$c = $r['code'];
|
||||
if ($c === 0) {
|
||||
$e = $r['error'];
|
||||
$hint = stripos($e, 'ssl') !== false || stripos($e, 'certificate') !== false
|
||||
? ' Tipp: Hat die NAS ein selbst erstelltes Zertifikat, schalte die Zertifikatsprüfung in den Einstellungen aus (weniger sicher) oder installiere ein Zertifikat von Let’s Encrypt.' : '';
|
||||
return "{$what}: keine Verbindung ({$e}).{$hint}";
|
||||
}
|
||||
return match (true) {
|
||||
$c === 401 => "{$what}: Anmeldung abgelehnt. Benutzer oder Passwort stimmen nicht (bei Synology: Benutzer braucht Zugriff auf WebDAV und den Ordner).",
|
||||
$c === 403 => "{$what}: Zugriff verweigert (HTTP 403). Der Benutzer darf dort nicht schreiben.",
|
||||
$c === 404 => "{$what}: Pfad nicht gefunden (HTTP 404). Bei Synology lautet die Adresse https://<nas>:5006/<Name des freigegebenen Ordners>.",
|
||||
$c === 405 => "{$what}: Der Server erlaubt diese Aktion nicht (HTTP 405).",
|
||||
$c === 507 => "{$what}: Auf der NAS ist kein Speicherplatz mehr frei (HTTP 507).",
|
||||
default => "{$what}: unerwartete Antwort des Servers (HTTP {$c}).",
|
||||
};
|
||||
}
|
||||
|
||||
/** Legt den Ordner (auch verschachtelt) an; vorhandene Ordner sind in Ordnung. Freigaben (oberste Ebene) der NAS müssen schon existieren. */
|
||||
function nasbk_ensure_folder(array $cfg): ?string
|
||||
{
|
||||
if ($cfg['folder'] === '') return null;
|
||||
$exists = function () use ($cfg): bool {
|
||||
$r = nasbk_request($cfg, 'PROPFIND', nasbk_remote_url($cfg) . '/', ['body' => '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/></d:prop></d:propfind>', 'headers' => ['Depth: 0', 'Content-Type: application/xml'], 'timeout' => 30]);
|
||||
return $r['code'] === 207;
|
||||
};
|
||||
if ($exists()) return null;
|
||||
$url = $cfg['url'];
|
||||
$segments = explode('/', $cfg['folder']);
|
||||
$last = count($segments) - 1;
|
||||
foreach ($segments as $i => $seg) {
|
||||
$url .= '/' . rawurlencode($seg);
|
||||
$r = nasbk_request($cfg, 'MKCOL', $url, ['timeout' => 30]);
|
||||
if ($r['code'] === 401) return nasbk_explain($r, 'Ordner anlegen');
|
||||
// Zwischenebenen gibt es meist schon (405) oder dürfen nicht angelegt werden (403, 409): erst das Ergebnis am Ende zählt
|
||||
if ($i === $last && !in_array($r['code'], [200, 201, 204, 301, 405], true) && !$exists()) return nasbk_explain($r, 'Ordner anlegen');
|
||||
}
|
||||
return $exists() ? null : 'Ordner anlegen: Der Zielordner „' . $cfg['folder'] . '“ existiert nicht und konnte nicht angelegt werden. Lege ihn auf der NAS an oder prüfe den Pfad (er beginnt mit dem Namen des freigegebenen Ordners).';
|
||||
}
|
||||
/** Liest die Dateien im Ordner. @return array{ok:bool,items:array<int,array{name:string,size:int,modified:string}>,error:string} */
|
||||
function nasbk_list(array $cfg, int $timeout = 30): array
|
||||
{
|
||||
$body = '<?xml version="1.0" encoding="utf-8"?><d:propfind xmlns:d="DAV:"><d:prop><d:getcontentlength/><d:getlastmodified/><d:resourcetype/></d:prop></d:propfind>';
|
||||
$r = nasbk_request($cfg, 'PROPFIND', nasbk_remote_url($cfg) . '/', ['body' => $body, 'headers' => ['Depth: 1', 'Content-Type: application/xml'], 'timeout' => $timeout]);
|
||||
if ($r['code'] === 404) return ['ok' => true, 'items' => [], 'error' => ''];
|
||||
if ($r['code'] !== 207) return ['ok' => false, 'items' => [], 'error' => nasbk_explain($r, 'Liste der Sicherungen')];
|
||||
$prev = libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($r['body'], 'SimpleXMLElement', LIBXML_NONET);
|
||||
libxml_use_internal_errors($prev);
|
||||
if ($xml === false) return ['ok' => false, 'items' => [], 'error' => 'Die Antwort der NAS ließ sich nicht lesen.'];
|
||||
$xml->registerXPathNamespace('d', 'DAV:');
|
||||
$items = [];
|
||||
foreach ($xml->xpath('//d:response') ?: [] as $resp) {
|
||||
$resp->registerXPathNamespace('d', 'DAV:');
|
||||
$href = (string) ($resp->xpath('d:href')[0] ?? '');
|
||||
$name = basename(rawurldecode(rtrim($href, '/')));
|
||||
if ($name === '' || !preg_match(nasbk_name_pattern(), $name)) continue;
|
||||
$items[] = [
|
||||
'name' => $name,
|
||||
'size' => (int) ($resp->xpath('.//d:getcontentlength')[0] ?? 0),
|
||||
'modified' => (string) ($resp->xpath('.//d:getlastmodified')[0] ?? ''),
|
||||
];
|
||||
}
|
||||
usort($items, fn($a, $b) => strcmp($b['name'], $a['name'])); // Namen enthalten das Datum: neueste zuerst
|
||||
return ['ok' => true, 'items' => $items, 'error' => ''];
|
||||
}
|
||||
|
||||
/** Nur Dateien, die dieses Plugin selbst anlegt, werden aufgelistet und bei der Aufbewahrung gelöscht. */
|
||||
function nasbk_name_pattern(): string
|
||||
{
|
||||
return '/^backup_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:_[a-z0-9]{3})?\.zip$/';
|
||||
}
|
||||
|
||||
/** Größe einer Datei auf dem Server (null, wenn sie sich nicht ermitteln lässt). */
|
||||
function nasbk_remote_size(array $cfg, string $url): ?int
|
||||
{
|
||||
$r = nasbk_request($cfg, 'PROPFIND', $url, ['body' => '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:getcontentlength/></d:prop></d:propfind>', 'headers' => ['Depth: 0', 'Content-Type: application/xml'], 'timeout' => 30]);
|
||||
if ($r['code'] === 207 && preg_match('#<(?:[a-z0-9]+:)?getcontentlength[^>]*>\s*(\d+)\s*<#i', $r['body'], $m)) return (int) $m[1];
|
||||
$h = nasbk_request($cfg, 'HEAD', $url, ['timeout' => 30]);
|
||||
if (in_array($h['code'], [200, 204], true) && preg_match('/^Content-Length:\s*(\d+)/im', $h['body'], $m)) return (int) $m[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Lädt die Datei hoch und prüft die Größe. Gibt eine Fehlermeldung oder null zurück. */
|
||||
function nasbk_upload(array $cfg, string $localPath, string $name): ?string
|
||||
{
|
||||
$url = nasbk_remote_url($cfg, $name);
|
||||
$size = (int) filesize($localPath);
|
||||
$timeout = max(300, (int) ceil($size / 1048576) * 20); // mindestens 5 Minuten, sonst rund 20 s je MB
|
||||
$r = nasbk_request($cfg, 'PUT', $url, ['file' => $localPath, 'timeout' => $timeout]);
|
||||
if (!in_array($r['code'], [200, 201, 204], true)) return nasbk_explain($r, 'Hochladen');
|
||||
$remote = nasbk_remote_size($cfg, $url);
|
||||
if ($remote !== null && $remote !== $size) {
|
||||
nasbk_request($cfg, 'DELETE', $url, ['timeout' => 30]);
|
||||
return "Hochladen: Die Datei auf der NAS ist unvollständig ({$remote} statt {$size} Bytes) und wurde wieder gelöscht.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Löscht Sicherungen nach den beiden Regeln „älter als X Tage“ und „mehr als Y Stück“ (0 = Regel aus). @return array{deleted:string[],error:string} */
|
||||
function nasbk_rotate(array $cfg): array
|
||||
{
|
||||
if ($cfg['keep'] <= 0 && $cfg['keep_days'] <= 0) return ['deleted' => [], 'error' => ''];
|
||||
$list = nasbk_list($cfg);
|
||||
if (!$list['ok']) return ['deleted' => [], 'error' => $list['error']];
|
||||
$items = $list['items']; // neueste zuerst (Namen enthalten das Datum)
|
||||
$drop = [];
|
||||
if ($cfg['keep_days'] > 0) {
|
||||
$cutoff = strtotime("-{$cfg['keep_days']} days");
|
||||
foreach ($items as $it) {
|
||||
if (preg_match('/^backup_(\d{4}-\d{2}-\d{2})_(\d{2})-(\d{2})-(\d{2})/', $it['name'], $m) && strtotime("{$m[1]} {$m[2]}:{$m[3]}:{$m[4]}") < $cutoff) $drop[$it['name']] = true;
|
||||
}
|
||||
}
|
||||
if ($cfg['keep'] > 0) foreach (array_slice($items, $cfg['keep']) as $it) $drop[$it['name']] = true;
|
||||
$deleted = [];
|
||||
foreach (array_keys($drop) as $name) {
|
||||
$r = nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, $name), ['timeout' => 60]);
|
||||
if (in_array($r['code'], [200, 204, 404], true)) $deleted[] = $name;
|
||||
}
|
||||
return ['deleted' => $deleted, 'error' => ''];
|
||||
}
|
||||
/** Hauptordner von DeskHub (Ort der Datei, die get_db() enthält: includes/db.php). */
|
||||
function nasbk_root(): string
|
||||
{
|
||||
return dirname((new ReflectionFunction('get_db'))->getFileName(), 2);
|
||||
}
|
||||
|
||||
/** Löscht einen Ordner samt Inhalt (nur der eigene Zwischenordner im Temp-Verzeichnis). */
|
||||
function nasbk_rmtree(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir) || !str_contains(basename($dir), 'deskhub-nasbk-')) return;
|
||||
$walk = function (string $d) use (&$walk): void {
|
||||
foreach (scandir($d) ?: [] as $e) {
|
||||
if ($e === '.' || $e === '..') continue;
|
||||
$p = $d . '/' . $e;
|
||||
is_dir($p) && !is_link($p) ? $walk($p) : @unlink($p);
|
||||
}
|
||||
@rmdir($d);
|
||||
};
|
||||
$walk($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt das Backup in einem Zwischenordner (außerhalb des Webverzeichnisses) mit den Einstellungen des Plugins:
|
||||
* Datenbank und/oder Dateien (wichtigste Dateien oder alles), ohne die ausgeschlossenen Pfade.
|
||||
*
|
||||
* @return array{ok:bool,error?:string,path?:string,filename?:string,size?:float,tmp?:string}
|
||||
*/
|
||||
function nasbk_make_archive(array $cfg): array
|
||||
{
|
||||
$root = nasbk_root();
|
||||
if (!function_exists('create_backup')) require_once $root . '/includes/backup.php';
|
||||
if (!function_exists('backup_verify')) require_once $root . '/includes/backup_restore.php';
|
||||
if (!$cfg['with_database'] && !$cfg['with_files']) return ['ok' => false, 'error' => 'Weder Datenbank noch Dateien sind zum Sichern ausgewählt.'];
|
||||
$tmp = sys_get_temp_dir() . '/deskhub-nasbk-' . bin2hex(random_bytes(4));
|
||||
if (!@mkdir($tmp . '/out', 0700, true)) return ['ok' => false, 'error' => 'Der Zwischenordner für das Backup konnte nicht angelegt werden.'];
|
||||
$options = ['with_database' => $cfg['with_database'], 'with_files' => $cfg['with_files'], 'exclude' => $cfg['excludes'], 'verify' => false]; // das Plugin prüft selbst (ohne Eintrag in die Prüfliste der Backup-Seite)
|
||||
// „Wichtigste Dateien“: Daten, die sich nicht neu herunterladen lassen (Uploads, Plugins, Konfiguration, Logo/Branding)
|
||||
if ($cfg['with_files'] && $cfg['scope'] === 'important') $options['only'] = ['uploads', 'plugins', 'config', 'assets/branding', '.htaccess'];
|
||||
$GLOBALS['nasbk_creating'] = true; // eigenes Backup des Plugins: nicht zusätzlich in die Warteschlange der Backup-Seite aufnehmen
|
||||
try {
|
||||
$res = create_backup($root, $tmp . '/out', null, $options);
|
||||
} finally {
|
||||
$GLOBALS['nasbk_creating'] = false;
|
||||
}
|
||||
if (empty($res['ok'])) { nasbk_rmtree($tmp); return ['ok' => false, 'error' => (string) ($res['error'] ?? 'Backup fehlgeschlagen.')]; }
|
||||
if ($cfg['with_database']) {
|
||||
$check = backup_verify($res['path']);
|
||||
if (empty($check['ok'])) { nasbk_rmtree($tmp); return ['ok' => false, 'error' => 'Das erzeugte Backup ist nicht in Ordnung: ' . implode(' ', (array) ($check['errors'] ?? []))]; }
|
||||
} else {
|
||||
$z = new ZipArchive();
|
||||
if ($z->open($res['path']) !== true || $z->numFiles < 1) { nasbk_rmtree($tmp); return ['ok' => false, 'error' => 'Das erzeugte Backup lässt sich nicht öffnen oder ist leer.']; }
|
||||
$z->close();
|
||||
}
|
||||
return ['ok' => true, 'path' => $res['path'], 'filename' => $res['filename'], 'size' => (float) $res['size'], 'tmp' => $tmp];
|
||||
}
|
||||
/** Hält das Ergebnis fest (Anzeige auf der Einstellungsseite) und meldet Fehler höchstens einmal am Tag per E-Mail. */
|
||||
function nasbk_record(bool $ok, string $message, bool $manual): void
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
plugin_set_setting(NASBK, 'last_run', $now);
|
||||
plugin_set_setting(NASBK, 'last_ok', $ok ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'last_message', $message);
|
||||
if ($ok) plugin_set_setting(NASBK, 'last_ok_at', $now);
|
||||
$hist = json_decode((string) plugin_setting(NASBK, 'history', '[]'), true);
|
||||
$hist = is_array($hist) ? $hist : [];
|
||||
array_unshift($hist, ['time' => $now, 'ok' => $ok, 'message' => $message, 'manual' => $manual]);
|
||||
plugin_set_setting(NASBK, 'history', json_encode(array_slice($hist, 0, 15), JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (!$ok && !$manual) {
|
||||
$last = strtotime((string) plugin_setting(NASBK, 'last_mail', '')) ?: 0;
|
||||
$to = (string) (get_setting('admin_email', '') ?: (config()['site']['admin_email'] ?? ''));
|
||||
if ($to !== '' && time() - $last > 86400 && function_exists('send_mail')) {
|
||||
plugin_set_setting(NASBK, 'last_mail', $now);
|
||||
@send_mail($to, 'DeskHub: NAS-Backup fehlgeschlagen', '<p>Das automatische Backup auf die NAS ist fehlgeschlagen:</p><p><strong>' . e($message) . '</strong></p><p>Details und Einstellungen: Adminportal → NAS-Backup.</p>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Führt ein Backup samt Upload und Aufbewahrung aus. @return array{ok:bool,message:string} */
|
||||
function nasbk_run(PDO $pdo, bool $manual = false): array
|
||||
{
|
||||
@set_time_limit(0);
|
||||
@ignore_user_abort(true);
|
||||
$cfg = nasbk_config();
|
||||
if ($err = nasbk_config_error($cfg)) { nasbk_record(false, $err, $manual); return ['ok' => false, 'message' => $err]; }
|
||||
|
||||
$arch = nasbk_make_archive($cfg);
|
||||
if (!$arch['ok']) { nasbk_record(false, $arch['error'], $manual); return ['ok' => false, 'message' => $arch['error']]; }
|
||||
try {
|
||||
if ($err = nasbk_ensure_folder($cfg)) { nasbk_record(false, $err, $manual); return ['ok' => false, 'message' => $err]; }
|
||||
if ($err = nasbk_upload($cfg, $arch['path'], $arch['filename'])) { nasbk_record(false, $err, $manual); return ['ok' => false, 'message' => $err]; }
|
||||
$rot = nasbk_rotate($cfg);
|
||||
$msg = $arch['filename'] . ' (' . $arch['size'] . ' MB, ' . ($cfg['mode'] === 'database' ? 'nur Datenbank' : 'komplett') . ') auf die NAS hochgeladen'
|
||||
. ($rot['deleted'] ? '; ' . count($rot['deleted']) . ' alte Sicherung(en) gelöscht' : '')
|
||||
. ($rot['error'] !== '' ? '; Aufbewahrung nicht ausgeführt: ' . $rot['error'] : '') . '.';
|
||||
nasbk_record(true, $msg, $manual);
|
||||
return ['ok' => true, 'message' => $msg];
|
||||
} finally {
|
||||
nasbk_rmtree($arch['tmp']);
|
||||
}
|
||||
}
|
||||
|
||||
/** Läuft gerade ein manuell gestartetes Backup? (Markierung verfällt nach 2 Stunden, falls ein Lauf abgebrochen wurde) */
|
||||
function nasbk_running(): bool
|
||||
{
|
||||
$t = (int) plugin_setting(NASBK, 'running_since', '0');
|
||||
return $t > 0 && time() - $t < 7200;
|
||||
}
|
||||
|
||||
/** Startet ein eigenes Backup nach dem Senden der Antwort (das Seitenladen wartet nicht auf das Backup). */
|
||||
function nasbk_run_background(PDO $pdo): void
|
||||
{
|
||||
plugin_set_setting(NASBK, 'running_since', (string) time());
|
||||
register_shutdown_function(function () use ($pdo) {
|
||||
if (function_exists('fastcgi_finish_request')) @fastcgi_finish_request();
|
||||
@ignore_user_abort(true);
|
||||
try {
|
||||
nasbk_run($pdo, true);
|
||||
} catch (Throwable $e) {
|
||||
nasbk_record(false, 'Backup abgebrochen: ' . $e->getMessage(), true);
|
||||
} finally {
|
||||
plugin_set_setting(NASBK, 'running_since', '0');
|
||||
}
|
||||
});
|
||||
}
|
||||
/** Prüft die Verbindung: Ordner anlegen, kleine Testdatei schreiben, Größe lesen, löschen. @return array{ok:bool,message:string} */
|
||||
function nasbk_test(array $cfg): array
|
||||
{
|
||||
if ($err = nasbk_config_error($cfg)) return ['ok' => false, 'message' => $err];
|
||||
if ($err = nasbk_ensure_folder($cfg)) return ['ok' => false, 'message' => $err];
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'nasbk');
|
||||
file_put_contents($tmp, 'DeskHub NAS-Backup Verbindungstest ' . date('c'));
|
||||
$name = 'verbindungstest-' . bin2hex(random_bytes(3)) . '.txt';
|
||||
$url = nasbk_remote_url($cfg, $name);
|
||||
$put = nasbk_request($cfg, 'PUT', $url, ['file' => $tmp, 'timeout' => 60]);
|
||||
@unlink($tmp);
|
||||
if (!in_array($put['code'], [200, 201, 204], true)) return ['ok' => false, 'message' => nasbk_explain($put, 'Testdatei schreiben')];
|
||||
$del = nasbk_request($cfg, 'DELETE', $url, ['timeout' => 30]);
|
||||
$note = in_array($del['code'], [200, 204], true) ? '' : ' Die Testdatei konnte nicht wieder gelöscht werden (Löschrecht prüfen, sonst bleiben alte Sicherungen liegen).';
|
||||
return ['ok' => true, 'message' => 'Verbindung in Ordnung: Der Ordner „' . ($cfg['folder'] ?: '/') . '“ ist erreichbar und beschreibbar.' . $note];
|
||||
}
|
||||
|
||||
/** Wird alle ~5 Minuten vom Plugin-Takt aufgerufen: kopiert neue Backups des Servers auf die NAS und startet ein eigenes Backup, wenn es laut Zeitplan fällig ist. */
|
||||
function nasbk_tick(PDO $pdo): void
|
||||
{
|
||||
$cfg = nasbk_config();
|
||||
if (!$cfg['enabled'] || nasbk_config_error($cfg)) return;
|
||||
nasbk_sync_local($pdo, false);
|
||||
if (!$cfg['own_schedule']) return;
|
||||
$interval = nasbk_intervals()[$cfg['interval']][1];
|
||||
$now = time();
|
||||
$lastOk = strtotime((string) plugin_setting(NASBK, 'last_ok_at', '')) ?: 0;
|
||||
$lastRun = strtotime((string) plugin_setting(NASBK, 'last_run', '')) ?: 0;
|
||||
if ($now - $lastOk < $interval - min(3600, intdiv($interval, 10))) return; // noch nicht fällig
|
||||
if ($interval >= 86400 && $lastOk > 0 && date('H:i') < $cfg['time']) return; // täglich/wöchentlich/monatlich erst ab der eingestellten Uhrzeit
|
||||
if ($now - $lastRun < 3600) return; // nach einem Fehler frühestens nach einer Stunde erneut
|
||||
if (function_exists('pseudo_cron_try_claim') && !pseudo_cron_try_claim($pdo, 'plugin_nas_backup_lock', 600)) return;
|
||||
nasbk_run($pdo, false);
|
||||
}
|
||||
// ─── Zusammenarbeit mit der Backup-Seite ─────────────────────────────────────
|
||||
|
||||
/** Dateiname eines Backups, das sich sicher verwenden lässt (kein Pfad, nur .zip im Namensschema). */
|
||||
function nasbk_valid_name(string $name): bool
|
||||
{
|
||||
return $name === basename($name) && preg_match(nasbk_name_pattern(), $name) === 1;
|
||||
}
|
||||
|
||||
/** Ordner mit den lokalen Backups von DeskHub. */
|
||||
function nasbk_local_dir(): string
|
||||
{
|
||||
// Nur im Testbetrieb (DESKHUB_TESTING=1) darf ein anderer Ordner gesetzt werden
|
||||
if (getenv('DESKHUB_TESTING') === '1' && getenv('DESKHUB_BACKUP_DIR')) return rtrim((string) getenv('DESKHUB_BACKUP_DIR'), '/\\');
|
||||
return nasbk_root() . '/backups';
|
||||
}
|
||||
|
||||
/** Pfad einer lokalen Sicherung oder null. */
|
||||
function nasbk_local_path(string $name): ?string
|
||||
{
|
||||
if (!nasbk_valid_name($name)) return null;
|
||||
$dir = realpath(nasbk_local_dir());
|
||||
$p = $dir ? $dir . '/' . $name : null;
|
||||
return $p && is_file($p) ? $p : null;
|
||||
}
|
||||
|
||||
/** Lädt eine lokale Sicherung auf die NAS, kontrolliert die Größe und räumt nach der Aufbewahrungsregel auf. @return array{ok:bool,message:string} */
|
||||
function nasbk_push_file(array $cfg, string $path, string $name): array
|
||||
{
|
||||
@set_time_limit(0);
|
||||
if ($err = nasbk_config_error($cfg)) return ['ok' => false, 'message' => $err];
|
||||
if ($err = nasbk_ensure_folder($cfg)) return ['ok' => false, 'message' => $err];
|
||||
if ($err = nasbk_upload($cfg, $path, $name)) return ['ok' => false, 'message' => $err];
|
||||
$rot = nasbk_rotate($cfg);
|
||||
nasbk_cache_clear();
|
||||
return ['ok' => true, 'message' => $name . ' auf die NAS kopiert' . ($rot['deleted'] ? ' (' . count($rot['deleted']) . ' alte Sicherung(en) dort gelöscht)' : '') . '.'];
|
||||
}
|
||||
|
||||
/** Zwischenspeicher der NAS-Liste (damit die Backup-Seite nicht bei jedem Aufruf die NAS befragt). */
|
||||
function nasbk_cache_clear(): void
|
||||
{
|
||||
plugin_set_setting(NASBK, 'remote_cache', '');
|
||||
}
|
||||
|
||||
/** @return array{ok:bool,items:array,error:string,at:int} */
|
||||
function nasbk_remote_cached(array $cfg): array
|
||||
{
|
||||
$c = json_decode((string) plugin_setting(NASBK, 'remote_cache', ''), true);
|
||||
if (is_array($c) && time() - (int) ($c['at'] ?? 0) < (!empty($c['ok']) ? 60 : 180)) return $c;
|
||||
$l = nasbk_list($cfg, 8);
|
||||
$l['at'] = time();
|
||||
plugin_set_setting(NASBK, 'remote_cache', json_encode($l, JSON_UNESCAPED_UNICODE));
|
||||
return $l;
|
||||
}
|
||||
|
||||
/** Speichert die Einstellungen aus dem Formular. @return array{ok:bool,message:string} */
|
||||
function nasbk_save_settings(array $post, array $staff = []): array
|
||||
{
|
||||
$host = trim((string) ($post['host'] ?? ''));
|
||||
$host = preg_replace('#^https?://#i', '', $host);
|
||||
$host = rtrim(explode('/', $host)[0], ':');
|
||||
$https = !empty($post['https']);
|
||||
$port = (int) ($post['port'] ?? 0);
|
||||
if ($port < 1 || $port > 65535) $port = $https ? 443 : 80;
|
||||
$target = '/' . trim(trim((string) ($post['target'] ?? '')), '/') . '/';
|
||||
$default = ($https && $port === 443) || (!$https && $port === 80);
|
||||
$err = nasbk_url_error(($https ? 'https' : 'http') . '://' . $host . ($default ? '' : ':' . $port));
|
||||
if (!$err && empty($post['db']) && empty($post['files']) && !empty($post['own_schedule'])) $err = 'Für den eigenen Zeitplan bitte mindestens „Datenbank“ oder „Dateien“ auswählen.';
|
||||
if ($err) return ['ok' => false, 'message' => $err];
|
||||
$interval = (string) ($post['interval'] ?? 'daily');
|
||||
$time = (string) ($post['time'] ?? '02:00');
|
||||
plugin_set_setting(NASBK, 'enabled', !empty($post['enabled']) ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'host', $host);
|
||||
plugin_set_setting(NASBK, 'port', (string) $port);
|
||||
plugin_set_setting(NASBK, 'https', $https ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'url', ''); // alte Einstellung durch die neuen Felder ersetzt
|
||||
plugin_set_setting(NASBK, 'folder', '');
|
||||
plugin_set_setting(NASBK, 'target', $target);
|
||||
plugin_set_setting(NASBK, 'user', trim((string) ($post['user'] ?? '')));
|
||||
$pw = (string) ($post['password'] ?? '');
|
||||
if ($pw !== '') plugin_set_setting(NASBK, 'password', encrypt_secret($pw)); // leer = gespeichertes Passwort behalten
|
||||
plugin_set_setting(NASBK, 'verify_tls', !empty($post['verify_tls']) ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'mirror', !empty($post['mirror']) ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'own_schedule', !empty($post['own_schedule']) ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'db', !empty($post['db']) ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'files', !empty($post['files']) ? '1' : '0');
|
||||
plugin_set_setting(NASBK, 'scope', ($post['scope'] ?? 'important') === 'full' ? 'full' : 'important');
|
||||
plugin_set_setting(NASBK, 'excludes', mb_substr(trim((string) ($post['excludes'] ?? '')), 0, 4000));
|
||||
plugin_set_setting(NASBK, 'interval', isset(nasbk_intervals()[$interval]) ? $interval : 'daily');
|
||||
plugin_set_setting(NASBK, 'time', preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $time) ? $time : '02:00');
|
||||
plugin_set_setting(NASBK, 'keep_days', (string) max(0, min(3650, (int) ($post['keep_days'] ?? 0))));
|
||||
plugin_set_setting(NASBK, 'keep', (string) max(0, min(365, (int) ($post['keep'] ?? 14))));
|
||||
nasbk_cache_clear();
|
||||
return ['ok' => true, 'message' => 'Einstellungen gespeichert.'];
|
||||
}
|
||||
|
||||
/** Holt eine Sicherung von der NAS in den Backup-Ordner und prüft sie. @return array{ok:bool,message:string} */
|
||||
function nasbk_fetch(array $cfg, string $name): array
|
||||
{
|
||||
@set_time_limit(0);
|
||||
if (!nasbk_valid_name($name)) return ['ok' => false, 'message' => 'Ungültiger Dateiname.'];
|
||||
if ($err = nasbk_config_error($cfg)) return ['ok' => false, 'message' => $err];
|
||||
$dir = realpath(nasbk_local_dir());
|
||||
if (!$dir || !is_writable($dir)) return ['ok' => false, 'message' => 'Der Ordner backups auf dem Server ist nicht beschreibbar.'];
|
||||
$target = $dir . '/' . $name;
|
||||
if (is_file($target)) return ['ok' => false, 'message' => 'Diese Sicherung liegt schon auf dem Server.'];
|
||||
$tmp = $dir . '/.nas-' . bin2hex(random_bytes(4)) . '.part';
|
||||
$r = nasbk_request($cfg, 'GET', nasbk_remote_url($cfg, $name), ['save_to' => $tmp, 'timeout' => 3600]);
|
||||
if ($r['code'] !== 200) { @unlink($tmp); return ['ok' => false, 'message' => nasbk_explain($r, 'Herunterladen')]; }
|
||||
$expected = nasbk_remote_size($cfg, nasbk_remote_url($cfg, $name));
|
||||
if ($expected !== null && filesize($tmp) !== $expected) { @unlink($tmp); return ['ok' => false, 'message' => 'Herunterladen: Die Datei ist unvollständig angekommen. Bitte erneut versuchen.']; }
|
||||
if (!function_exists('backup_verify')) require_once nasbk_root() . '/includes/backup_restore.php';
|
||||
$check = backup_verify($tmp);
|
||||
if (empty($check['ok'])) { @unlink($tmp); return ['ok' => false, 'message' => 'Die Sicherung von der NAS ist nicht in Ordnung: ' . implode(' ', (array) ($check['errors'] ?? []))]; }
|
||||
if (!@rename($tmp, $target)) { @unlink($tmp); return ['ok' => false, 'message' => 'Die Datei konnte nicht in den Backup-Ordner verschoben werden.']; }
|
||||
if (function_exists('backup_verify_and_record')) backup_verify_and_record($target);
|
||||
return ['ok' => true, 'message' => $name . ' von der NAS geholt und geprüft. Du kannst sie jetzt in der Liste wiederherstellen.'];
|
||||
}
|
||||
|
||||
// ─── Kopieren der Backups dieses Servers ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Kopiert Backups aus dem Backup-Ordner des Servers (die DeskHub selbst erstellt hat: Zeitplan der Seite „Backup“, manuell, vor Updates)
|
||||
* auf die NAS. Nur Backups, die nicht als defekt geprüft wurden; ohne Prüfergebnis erst nach 20 Minuten (die automatische Prüfung
|
||||
* läuft alle 10 Minuten). Betrachtet werden die neuesten Backups im Rahmen der Aufbewahrung (Anzahl/Alter), damit nicht alte
|
||||
* Sicherungen hochgeladen und gleich wieder gelöscht werden.
|
||||
*
|
||||
* @return array{ok:bool,done:int,message:string}
|
||||
*/
|
||||
function nasbk_sync_local(PDO $pdo, bool $manual = false, int $max = 2): array
|
||||
{
|
||||
$cfg = nasbk_config();
|
||||
if (!$manual && (!$cfg['enabled'] || !$cfg['mirror'])) return ['ok' => true, 'done' => 0, 'message' => ''];
|
||||
if ($e = nasbk_config_error($cfg)) return ['ok' => false, 'done' => 0, 'message' => $e];
|
||||
if (!$manual) {
|
||||
$failAt = strtotime((string) plugin_setting(NASBK, 'sync_fail_at', '')) ?: 0;
|
||||
if (time() - $failAt < 3600) return ['ok' => true, 'done' => 0, 'message' => '']; // nach einem Fehler frühestens nach einer Stunde erneut
|
||||
if (function_exists('pseudo_cron_try_claim') && !pseudo_cron_try_claim($pdo, 'plugin_nas_backup_sync', 300)) return ['ok' => true, 'done' => 0, 'message' => ''];
|
||||
}
|
||||
@set_time_limit(0);
|
||||
$dir = realpath(nasbk_local_dir());
|
||||
if (!$dir) return ['ok' => true, 'done' => 0, 'message' => ''];
|
||||
$files = [];
|
||||
foreach (glob($dir . '/backup_*.zip') ?: [] as $p) if (nasbk_valid_name(basename($p))) $files[$p] = filemtime($p);
|
||||
arsort($files);
|
||||
$limit = $cfg['keep'] > 0 ? $cfg['keep'] : 10;
|
||||
$files = array_slice($files, 0, $limit, true);
|
||||
if ($cfg['keep_days'] > 0) $files = array_filter($files, fn($t) => $t >= strtotime("-{$cfg['keep_days']} days"));
|
||||
if (!$files) return ['ok' => true, 'done' => 0, 'message' => ''];
|
||||
|
||||
$remote = nasbk_list($cfg, 20);
|
||||
if (!$remote['ok']) return nasbk_sync_fail($remote['error'], $manual);
|
||||
$have = [];
|
||||
foreach ($remote['items'] as $it) $have[$it['name']] = $it['size'];
|
||||
$checks = json_decode((string) get_setting('backup_check_map', ''), true);
|
||||
$checks = is_array($checks) ? $checks : [];
|
||||
|
||||
$done = 0;
|
||||
$skippedBad = 0;
|
||||
foreach ($files as $path => $mtime) {
|
||||
$name = basename($path);
|
||||
if (isset($have[$name]) && $have[$name] === filesize($path)) continue; // liegt schon vollständig auf der NAS
|
||||
$chk = $checks[$name] ?? null;
|
||||
if ($chk && empty($chk['ok'])) { $skippedBad++; continue; } // defekte Backups nicht kopieren
|
||||
if (!$chk && !$manual && time() - $mtime < 1200) continue; // noch nicht geprüft: kurz warten
|
||||
if (!$manual && $done >= $max) break;
|
||||
$r = nasbk_push_file($cfg, $path, $name);
|
||||
if (!$r['ok']) return nasbk_sync_fail($name . ': ' . $r['message'], $manual);
|
||||
nasbk_record(true, $r['message'], $manual);
|
||||
$done++;
|
||||
}
|
||||
if ($done > 0) nasbk_cache_clear();
|
||||
$msg = $done . ' Sicherung(en) auf die NAS kopiert' . ($skippedBad ? '; ' . $skippedBad . ' defekte(s) Backup(s) übersprungen' : '') . '.';
|
||||
return ['ok' => true, 'done' => $done, 'message' => $msg];
|
||||
}
|
||||
|
||||
/** Fehler beim Kopieren festhalten (E-Mail an die Admin-Adresse höchstens einmal am Tag) und weitere Versuche für eine Stunde aussetzen. */
|
||||
function nasbk_sync_fail(string $message, bool $manual): array
|
||||
{
|
||||
plugin_set_setting(NASBK, 'sync_fail_at', date('Y-m-d H:i:s'));
|
||||
nasbk_record(false, 'Kopie auf die NAS fehlgeschlagen: ' . $message, $manual);
|
||||
return ['ok' => false, 'done' => 0, 'message' => $message];
|
||||
}
|
||||
|
||||
/** Adresse der Plugin-Seite (Reiter unter „Backup“). */
|
||||
function nasbk_page_url(): string
|
||||
{
|
||||
return '/staff/plugin.php?p=' . NASBK . '&page=einstellungen';
|
||||
}
|
||||
|
||||
/** Kleines Formular, das eine Aktion dieser Seite auslöst ($target: andere Adresse, wenn es auf der Seite „Backup“ steht). */
|
||||
function nasbk_action_form(string $do, string $label, array $extra = [], string $confirm = '', string $class = 'secondary', string $style = '', string $target = ''): string
|
||||
{
|
||||
$h = '<form method="post" style="margin:0"' . ($target !== '' ? ' action="' . e($target) . '"' : '') . ($confirm !== '' ? ' onsubmit="return confirm(' . e(json_encode($confirm, JSON_UNESCAPED_UNICODE)) . ')"' : '') . '>' . csrf_field()
|
||||
. '<input type="hidden" name="do" value="' . e($do) . '">';
|
||||
foreach ($extra as $k => $v) $h .= '<input type="hidden" name="' . e($k) . '" value="' . e($v) . '">';
|
||||
return $h . '<button type="submit" class="btn ' . e($class) . ' bk-btn" style="padding:4px 9px;font-size:.76rem;white-space:nowrap' . ($style !== '' ? ';' . e($style) : '') . '">' . e($label) . '</button></form>';
|
||||
}
|
||||
|
||||
/** Aktionen der Plugin-Seite. @return array{ok:bool,message:string} */
|
||||
function nasbk_page_action(string $do, array $post, array $staff = []): array
|
||||
{
|
||||
$cfg = nasbk_config();
|
||||
$name = (string) ($post['file'] ?? '');
|
||||
switch ($do) {
|
||||
case 'save':
|
||||
return nasbk_save_settings($post, $staff);
|
||||
case 'test':
|
||||
return nasbk_test($cfg);
|
||||
case 'run':
|
||||
if (nasbk_running()) return ['ok' => false, 'message' => 'Es läuft bereits ein Backup. Bitte kurz warten.'];
|
||||
nasbk_run_background(get_db());
|
||||
return ['ok' => true, 'message' => 'Das eigene Backup läuft jetzt im Hintergrund. Das Ergebnis steht oben im Status; die Seite lädt sich von selbst neu.'];
|
||||
case 'sync':
|
||||
nasbk_cache_clear();
|
||||
$r = nasbk_sync_local(get_db(), true);
|
||||
return ['ok' => $r['ok'], 'message' => $r['message']];
|
||||
case 'upload':
|
||||
$path = nasbk_local_path($name);
|
||||
if ($path === null) return ['ok' => false, 'message' => 'Backup nicht gefunden.'];
|
||||
$r = nasbk_push_file($cfg, $path, $name);
|
||||
nasbk_record($r['ok'], $r['message'], true);
|
||||
return $r;
|
||||
case 'fetch':
|
||||
return nasbk_fetch($cfg, $name);
|
||||
case 'delete_remote':
|
||||
if (!nasbk_valid_name($name) || nasbk_config_error($cfg)) return ['ok' => false, 'message' => 'Ungültige Anfrage.'];
|
||||
$r = nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, $name), ['timeout' => 60]);
|
||||
nasbk_cache_clear();
|
||||
return in_array($r['code'], [200, 204], true) ? ['ok' => true, 'message' => $name . ' von der NAS gelöscht.'] : ['ok' => false, 'message' => nasbk_explain($r, 'Löschen')];
|
||||
}
|
||||
return ['ok' => false, 'message' => 'Unbekannte Aktion.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergänzt die Liste „Vorhandene Backups“ auf der Seite „Backup“ (Filter backup.list.rows): Marke „auf NAS“ / „nur hier“ bei den Backups des
|
||||
* Servers, Knopf „Auf NAS“ und Sicherungen, die nur auf der NAS liegen (mit „Auf Server holen“). Die Knöpfe senden an die Seite des Plugins.
|
||||
*/
|
||||
function nasbk_list_rows(array $rows, array $ctx = []): array
|
||||
{
|
||||
$cfg = nasbk_config();
|
||||
if (!$cfg['enabled'] || nasbk_config_error($cfg) !== null) return $rows;
|
||||
$remote = nasbk_remote_cached($cfg);
|
||||
$url = nasbk_page_url();
|
||||
$byName = [];
|
||||
if ($remote['ok']) foreach ($remote['items'] as $it) $byName[$it['name']] = $it;
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
if (($r['source'] ?? 'local') !== 'local' || !nasbk_valid_name((string) $r['name'])) continue;
|
||||
if (!$remote['ok']) { $r['badges'][] = '<span class="bk-badge" title="' . e('Die NAS ließ sich nicht abfragen: ' . $remote['error']) . '">☁ unbekannt</span>'; continue; }
|
||||
$bytes = (int) ($r['bytes'] ?? round(((float) $r['size']) * 1048576));
|
||||
if (isset($byName[$r['name']])) {
|
||||
$same = $byName[$r['name']]['size'] === $bytes;
|
||||
$r['badges'][] = $same ? '<span class="bk-badge ok" title="Diese Sicherung liegt vollständig auf der NAS">☁ auf NAS</span>' : '<span class="bk-badge bad" title="Die Datei auf der NAS hat eine andere Größe">☁ unvollständig</span>';
|
||||
if (!$same) $r['actions'] = nasbk_action_form('upload', '☁ Erneut auf NAS', ['file' => $r['name']], '', 'secondary', '', $url) . ($r['actions'] ?? '');
|
||||
} else {
|
||||
$r['badges'][] = '<span class="bk-badge" title="Noch nicht auf der NAS (wird automatisch kopiert, sobald das Backup geprüft ist)">nur hier</span>';
|
||||
$r['actions'] = nasbk_action_form('upload', '☁ Auf NAS', ['file' => $r['name']], '', 'secondary', '', $url) . ($r['actions'] ?? '');
|
||||
}
|
||||
}
|
||||
unset($r);
|
||||
if ($remote['ok']) {
|
||||
$localNames = array_column($rows, 'name');
|
||||
foreach ($remote['items'] as $it) {
|
||||
if (in_array($it['name'], $localNames, true)) continue;
|
||||
$rows[] = [
|
||||
'name' => $it['name'], 'size' => round($it['size'] / 1048576, 2), 'bytes' => $it['size'], 'date' => strtotime($it['modified']) ?: time(), 'source' => 'extern',
|
||||
'badges' => ['<span class="bk-badge ok" title="Liegt nur auf der NAS">☁ nur auf NAS</span>'],
|
||||
'actions' => nasbk_action_form('fetch', '⬇ Auf Server holen', ['file' => $it['name']], 'Diese Sicherung von der NAS auf den Server laden? Danach kannst du sie hier wiederherstellen.', 'secondary', '', $url)
|
||||
. nasbk_action_form('delete_remote', '🗑', ['file' => $it['name']], 'Diese Sicherung von der NAS löschen?', 'secondary', 'border-color:var(--danger);color:var(--danger)', $url),
|
||||
];
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?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 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 & 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>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"slug": "nas-backup",
|
||||
"name": "Synology NAS Backup",
|
||||
"version": "2.2.0",
|
||||
"description": "Kopiert die Backups des Servers (Zeitplan und Knopf der Seite Backup) automatisch per WebDAV auf eine Synology NAS oder einen anderen WebDAV-Server. Mit eigener Seite im Adminportal: Verbindung, Backup-Liste (auf dem Server / auf der NAS), Aufbewahrung nach Alter und Anzahl, optional eigener Zeitplan mit Inhalt und Ausschlüssen, Verbindungstest und Fehlermeldung per E-Mail.",
|
||||
"author": "DeskHub",
|
||||
"requires": "1.0.0",
|
||||
"permissions": ["hooks.cron", "ui.menu"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// Synology NAS Backup: kopiert die Backups dieses Servers per WebDAV auf eine NAS (alle ~5 Minuten geprüft) und hat eine eigene
|
||||
// Einstellungsseite im Adminportal (Reiter unter „Backup“). Braucht nur die Standard-Berechtigungen hooks.cron und ui.menu.
|
||||
require_once __DIR__ . '/lib.php';
|
||||
|
||||
plugin_add_hook('cron.run', function () {
|
||||
nasbk_tick(get_db());
|
||||
});
|
||||
|
||||
// Die Liste „Vorhandene Backups“ auf der Seite „Backup“ zeigt auch die Sicherungen der NAS (Marken, Knöpfe, nur auf der NAS liegende Einträge)
|
||||
plugin_add_filter('backup.list.rows', fn($rows, $ctx = []) => nasbk_list_rows((array) $rows, (array) $ctx));
|
||||
|
||||
// Einstellungen und Liste: als Reiter „Synology NAS Backup“ unter „Backup“ (kein eigener Menüpunkt), nur für Administratoren
|
||||
// (die Berechtigung kennt nur der Admin). Ältere DeskHub-Versionen ohne Reiter zeigen die Seite als normalen Menüpunkt.
|
||||
plugin_register_menu('Synology NAS Backup', 'einstellungen', 'manage_nas_backup', 'backup');
|
||||
Reference in New Issue
Block a user