730 lines
42 KiB
PHP
730 lines
42 KiB
PHP
<?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;
|
||
}
|