Upload via GUI (97 Dateien)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"slug": "lang-en",
|
||||
"name": "English (Sprachpaket)",
|
||||
"version": "1.0.1",
|
||||
"description": "Englische Oberfläche für Kundenportal und Mitarbeiterbereich. Nach der Aktivierung erscheint die britische Flagge zum Umschalten. Nicht übersetzte Texte bleiben deutsch.",
|
||||
"version": "1.1.0",
|
||||
"description": "Englische Oberfläche für Kundenportal und Mitarbeiterbereich. Nach der Aktivierung erscheint die britische Flagge zum Umschalten. Übersetzt Kundenportal, Mitarbeiterbereich sowie Meldungen und Fehlertexte. Nicht übersetzte Texte bleiben deutsch.",
|
||||
"author": "DeskHub",
|
||||
"entry": "plugin.php",
|
||||
"permissions": [],
|
||||
|
||||
+2851
-1140
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
const NASBK = 'nas-backup';
|
||||
/** Kleinste Teilgröße in Bytes (Tests überschreiben sie vorher mit define()). */
|
||||
if (!defined('NASBK_MIN_PART')) define('NASBK_MIN_PART', 10 * 1048576);
|
||||
|
||||
/** Zeitpläne: Schlüssel => [Bezeichnung, Mindestabstand in Sekunden]. */
|
||||
function nasbk_intervals(): array
|
||||
@@ -82,6 +84,10 @@ function nasbk_config(): array
|
||||
'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',
|
||||
// Große Backups in Teilen hochladen: automatisch (erst ganz, bei Fehler 413/Zeitüberschreitung in Teilen), immer oder nie
|
||||
'split' => in_array($splitMode = (string) plugin_setting(NASBK, 'split', 'auto'), ['auto', 'always', 'never'], true) ? $splitMode : 'auto',
|
||||
'part_mb' => max(10, min(4000, (int) plugin_setting(NASBK, 'part_mb', '200'))),
|
||||
'part_bytes' => max(10, min(4000, (int) plugin_setting(NASBK, 'part_mb', '200'))) * 1048576,
|
||||
];
|
||||
}
|
||||
/** 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. */
|
||||
@@ -151,12 +157,29 @@ function nasbk_request(array $cfg, string $method, string $url, array $opt = [])
|
||||
$set[CURLOPT_FILE] = $out;
|
||||
}
|
||||
if (isset($opt['file'])) {
|
||||
clearstatcache(true, $opt['file']); // dieselbe Zwischendatei wird für mehrere Teile neu beschrieben: sonst gilt die alte Größe
|
||||
$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['stream'])) {
|
||||
// Ein Abschnitt einer großen Datei direkt hochladen, ohne ihn vorher in eine Zwischendatei zu kopieren (spart bei großen Backups viel Plattenzugriff)
|
||||
$st = $opt['stream']; // ['handle' => Quelle, 'offset' => Beginn, 'length' => Länge, 'hash' => HashContext|null]
|
||||
$remaining = (int) $st['length'];
|
||||
fseek($st['handle'], (int) $st['offset']);
|
||||
$set[CURLOPT_UPLOAD] = true;
|
||||
$set[CURLOPT_INFILESIZE] = $remaining;
|
||||
$set[CURLOPT_READFUNCTION] = function ($curl, $fd, $length) use ($st, &$remaining) {
|
||||
if ($remaining <= 0) return '';
|
||||
$data = fread($st['handle'], min($length, $remaining, 1048576));
|
||||
if ($data === false || $data === '') return '';
|
||||
$remaining -= strlen($data);
|
||||
if (!empty($st['hash'])) hash_update($st['hash'], $data);
|
||||
return $data;
|
||||
};
|
||||
$headers[] = 'Expect:';
|
||||
} elseif (isset($opt['body'])) {
|
||||
$set[CURLOPT_POSTFIELDS] = $opt['body'];
|
||||
}
|
||||
@@ -187,6 +210,8 @@ function nasbk_explain(array $r, string $what): string
|
||||
$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).",
|
||||
$c === 413 => "{$what}: Die Datei ist größer, als der Server oder ein Proxy davor annimmt (HTTP 413). Häufige Ursachen: ein Reverse-Proxy (bei nginx „client_max_body_size“ erhöhen, bei der Synology unter Anmeldeportal → Erweitert → Reverse-Proxy den Header-/Größen-Grenzwert prüfen) oder Cloudflare (im Gratis-Tarif höchstens 100 MB je Upload; die NAS-Adresse dann ohne Cloudflare-Proxy, also „nur DNS“, erreichbar machen). Alternativ das Backup verkleinern: in den Einstellungen nur die Datenbank oder die „Wichtigsten Dateien“ sichern.",
|
||||
in_array($c, [408, 502, 504, 524], true) => "{$what}: Zeitüberschreitung beim Server oder Proxy vor der NAS (HTTP {$c}). Bei großen Dateien muss dort das Zeitlimit höher sein (bei Cloudflare gilt für Uploads zusätzlich eine Grenze von 100 MB im Gratis-Tarif).",
|
||||
default => "{$what}: unerwartete Antwort des Servers (HTTP {$c}).",
|
||||
};
|
||||
}
|
||||
@@ -212,34 +237,51 @@ function nasbk_ensure_folder(array $cfg): ?string
|
||||
}
|
||||
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} */
|
||||
/**
|
||||
* Liest die Dateien im Ordner. Große Backups liegen in Teilen auf der NAS (name.zip.part001, .part002 … und zuletzt eine kleine Datei
|
||||
* name.zip.parts, die den Satz als vollständig kennzeichnet). Diese Sätze erscheinen hier als EIN Backup mit der Gesamtgröße und `parts` = Anzahl.
|
||||
* Teile ohne Abschlussdatei (ein Upload, der abgebrochen wurde) stehen unter `orphans`.
|
||||
*
|
||||
* @return array{ok:bool,items:array<int,array{name:string,size:int,modified:string,parts:int}>,orphans:array<int,array{name:string,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')];
|
||||
if ($r['code'] === 404) return ['ok' => true, 'items' => [], 'orphans' => [], 'error' => ''];
|
||||
if ($r['code'] !== 207) return ['ok' => false, 'items' => [], 'orphans' => [], '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.'];
|
||||
if ($xml === false) return ['ok' => false, 'items' => [], 'orphans' => [], 'error' => 'Die Antwort der NAS ließ sich nicht lesen.'];
|
||||
$xml->registerXPathNamespace('d', 'DAV:');
|
||||
$items = [];
|
||||
$base = '(backup_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:_[a-z0-9]{3})?\.zip)';
|
||||
$singles = $parts = $markers = [];
|
||||
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] ?? ''),
|
||||
];
|
||||
$size = (int) ($resp->xpath('.//d:getcontentlength')[0] ?? 0);
|
||||
$mod = (string) ($resp->xpath('.//d:getlastmodified')[0] ?? '');
|
||||
if (preg_match("/^{$base}$/", $name)) $singles[$name] = ['name' => $name, 'size' => $size, 'modified' => $mod, 'parts' => 0];
|
||||
elseif (preg_match("/^{$base}\.part(\d{3})$/", $name, $m)) $parts[$m[1]][(int) $m[2]] = ['size' => $size, 'modified' => $mod, 'file' => $name];
|
||||
elseif (preg_match("/^{$base}\.parts$/", $name, $m)) $markers[$m[1]] = $mod;
|
||||
}
|
||||
$items = array_values($singles);
|
||||
$orphans = [];
|
||||
foreach ($parts as $logical => $set) {
|
||||
ksort($set);
|
||||
$complete = isset($markers[$logical]) && !isset($singles[$logical]) && array_keys($set) === range(1, count($set));
|
||||
if ($complete) {
|
||||
$items[] = ['name' => $logical, 'size' => array_sum(array_column($set, 'size')), 'modified' => $markers[$logical], 'parts' => count($set)];
|
||||
} else {
|
||||
foreach ($set as $p) $orphans[] = ['name' => $p['file'], 'modified' => $p['modified']];
|
||||
if (isset($markers[$logical])) $orphans[] = ['name' => $logical . '.parts', 'modified' => $markers[$logical]];
|
||||
}
|
||||
}
|
||||
usort($items, fn($a, $b) => strcmp($b['name'], $a['name'])); // Namen enthalten das Datum: neueste zuerst
|
||||
return ['ok' => true, 'items' => $items, 'error' => ''];
|
||||
return ['ok' => true, 'items' => $items, 'orphans' => $orphans, 'error' => ''];
|
||||
}
|
||||
|
||||
/** Nur Dateien, die dieses Plugin selbst anlegt, werden aufgelistet und bei der Aufbewahrung gelöscht. */
|
||||
function nasbk_name_pattern(): string
|
||||
{
|
||||
@@ -256,44 +298,276 @@ function nasbk_remote_size(array $cfg, string $url): ?int
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Lädt die Datei hoch und prüft die Größe. Gibt eine Fehlermeldung oder null zurück. */
|
||||
/** Name einer Teil-Datei auf der NAS: backup_….zip.part001 */
|
||||
function nasbk_part_name(string $name, int $n): string
|
||||
{
|
||||
return sprintf('%s.part%03d', $name, $n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Datei hoch und prüft die Größe. Gibt eine Fehlermeldung oder null zurück.
|
||||
*
|
||||
* Große Dateien scheitern oft an einer Größengrenze davor (Reverse-Proxy, Cloudflare: 100 MB). Deshalb gibt es zwei Wege:
|
||||
* - ganz (eine Datei, bequem zum Herunterladen von Hand) und
|
||||
* - in Teilen (je höchstens „Teilgröße“, danach eine kleine Abschlussdatei: erst damit gilt das Backup als vollständig).
|
||||
* Einstellung „automatisch“: erst ganz; schlägt das mit 413/Zeitüberschreitung/Verbindungsabbruch fehl, geht es in Teilen weiter und
|
||||
* die Größe wird gemerkt (ab dieser Größe gleich in Teilen, ohne die Datei noch einmal vergeblich zu senden).
|
||||
*/
|
||||
function nasbk_upload(array $cfg, string $localPath, string $name): ?string
|
||||
{
|
||||
$size = (int) filesize($localPath);
|
||||
$partBytes = (int) ($cfg['part_bytes'] ?? 200 * 1048576);
|
||||
$partLimit = (int) plugin_setting(NASBK, 'part_limit', '0'); // gemerkt: größere Teile lehnte der Server ab
|
||||
if ($partLimit > 0) $partBytes = min($partBytes, $partLimit);
|
||||
$mode = (string) ($cfg['split'] ?? 'auto');
|
||||
$learned = (int) plugin_setting(NASBK, 'single_limit', '0'); // gemerkt: ab dieser Größe scheiterte der Upload in einem Stück
|
||||
$canSplit = $mode !== 'never' && ($size > $partBytes || ($mode === 'auto' && $learned > 0 && $size >= $learned));
|
||||
if ($canSplit && ($mode === 'always' || ($learned > 0 && $size >= $learned))) return nasbk_upload_big($cfg, $localPath, $name, min($partBytes, max(NASBK_MIN_PART, intdiv($size, 2))));
|
||||
$res = nasbk_upload_single($cfg, $localPath, $name);
|
||||
if ($res['error'] === null) return null;
|
||||
// Scheitert die ganze Datei an einer Größengrenze, geht es in Teilen weiter (auch wenn die Datei kleiner als die eingestellte Teilgröße ist)
|
||||
if ($mode !== 'never' && $size > NASBK_MIN_PART && in_array($res['code'], [0, 408, 413, 502, 504, 524], true)) {
|
||||
plugin_set_setting(NASBK, 'single_limit', (string) ($learned > 0 ? min($learned, $size) : $size));
|
||||
return nasbk_upload_big($cfg, $localPath, $name, min($partBytes, max(NASBK_MIN_PART, intdiv($size, 2))));
|
||||
}
|
||||
return $res['error'];
|
||||
}
|
||||
|
||||
/** Eine Datei in einem Stück hochladen. @return array{error:?string,code:int} */
|
||||
function nasbk_upload_single(array $cfg, string $localPath, string $name): array
|
||||
{
|
||||
$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');
|
||||
if (!in_array($r['code'], [200, 201, 204], true)) return ['error' => nasbk_explain($r, 'Hochladen (' . round($size / 1048576, 1) . ' MB)'), 'code' => $r['code']];
|
||||
$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 ['error' => "Hochladen: Die Datei auf der NAS ist unvollständig ({$remote} statt {$size} Bytes) und wurde wieder gelöscht.", 'code' => 200];
|
||||
}
|
||||
return null;
|
||||
return ['error' => null, 'code' => $r['code']];
|
||||
}
|
||||
|
||||
/** 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} */
|
||||
/**
|
||||
* Große Datei in Stücken als EINE Datei hochladen (HTTP „Content-Range“: jedes Stück sagt, wohin es gehört; Synology DSM 7 kann das).
|
||||
* Auf der NAS liegt danach direkt die fertige ZIP, ohne Teile und ohne Zusammensetzen.
|
||||
*
|
||||
* Nach jedem Stück wird die Größe auf der NAS geprüft. Ignoriert ein Server „Content-Range“ stillschweigend (er würde die Datei bei jedem
|
||||
* Stück überschreiben und trotzdem „OK“ melden), fällt das auf und es wird als „nicht unterstützt“ gemeldet. Lehnt der Server ein Stück als
|
||||
* zu groß ab (HTTP 413), wird die Stückgröße halbiert und von vorn begonnen. Pro Stück bis zu 4 Versuche mit wachsender Wartezeit (3, 6, 12 s).
|
||||
* Bei einem Fehler wird die unvollständige Datei auf der NAS gelöscht.
|
||||
*
|
||||
* @return array{error:?string,code:int,unsupported:bool}
|
||||
*/
|
||||
function nasbk_upload_range(array $cfg, string $localPath, string $name, int $chunkBytes): array
|
||||
{
|
||||
@set_time_limit(0);
|
||||
$url = nasbk_remote_url($cfg, $name);
|
||||
clearstatcache(true, $localPath);
|
||||
$size = (int) filesize($localPath);
|
||||
$src = @fopen($localPath, 'rb');
|
||||
if ($src === false) return ['error' => 'Hochladen in Stücken: Die Backup-Datei konnte nicht gelesen werden.', 'code' => 0, 'unsupported' => false];
|
||||
$drop = fn() => nasbk_request($cfg, 'DELETE', $url, ['timeout' => 60]);
|
||||
$drop(); // Reste eines früheren Versuchs entfernen (sonst hängt die NAS „(2)“ an den Namen oder hängt Daten an)
|
||||
$chunk = max(NASBK_MIN_PART, $chunkBytes);
|
||||
while (true) {
|
||||
$total = max(1, (int) ceil($size / $chunk));
|
||||
$halve = false;
|
||||
for ($i = 0; $i < $total; $i++) {
|
||||
$offset = $i * $chunk;
|
||||
$len = min($chunk, $size - $offset);
|
||||
$end = $offset + $len - 1;
|
||||
$ok = false;
|
||||
$last = null;
|
||||
$lastCode = 0;
|
||||
for ($try = 1; $try <= 4 && !$ok; $try++) {
|
||||
$r = nasbk_request($cfg, 'PUT', $url, [
|
||||
'stream' => ['handle' => $src, 'offset' => $offset, 'length' => $len, 'hash' => null],
|
||||
'headers' => ["Content-Range: bytes {$offset}-{$end}/{$size}", 'Content-Type: application/octet-stream'],
|
||||
'timeout' => max(300, (int) ceil($len / 1048576) * 20),
|
||||
]);
|
||||
$lastCode = $r['code'];
|
||||
if (in_array($r['code'], [200, 201, 204, 206], true)) {
|
||||
$remote = nasbk_remote_size($cfg, $url);
|
||||
if ($remote === null || $remote === $offset + $len) { $ok = true; break; }
|
||||
// Die NAS hat „Content-Range“ nicht beachtet (Datei hat nicht die erwartete Größe)
|
||||
fclose($src);
|
||||
$drop();
|
||||
return ['error' => 'Hochladen in Stücken: Die NAS beachtet „Content-Range“ nicht.', 'code' => $r['code'], 'unsupported' => true];
|
||||
}
|
||||
if ($i === 0 && in_array($r['code'], [400, 405, 416, 501], true)) { // Server kennt Stück-Uploads nicht
|
||||
fclose($src);
|
||||
$drop();
|
||||
return ['error' => nasbk_explain($r, 'Hochladen in Stücken'), 'code' => $r['code'], 'unsupported' => true];
|
||||
}
|
||||
$last = nasbk_explain($r, "Hochladen von Stück " . ($i + 1) . " von {$total} (" . round($len / 1048576, 1) . ' MB)');
|
||||
if ($r['code'] === 413 || in_array($r['code'], [401, 403, 507], true)) break; // wiederholen hilft nicht
|
||||
if ($try < 4) sleep(min(60, 3 * (2 ** ($try - 1))));
|
||||
}
|
||||
if (!$ok) {
|
||||
if ($lastCode === 413 && $chunk > NASBK_MIN_PART) { $halve = true; break; }
|
||||
fclose($src);
|
||||
$drop();
|
||||
return ['error' => (string) $last, 'code' => $lastCode, 'unsupported' => false];
|
||||
}
|
||||
}
|
||||
if ($halve) {
|
||||
$chunk = max(NASBK_MIN_PART, intdiv($chunk, 2));
|
||||
plugin_set_setting(NASBK, 'part_limit', (string) $chunk); // künftig gleich mit dieser Größe beginnen
|
||||
$drop();
|
||||
continue;
|
||||
}
|
||||
fclose($src);
|
||||
return ['error' => null, 'code' => 201, 'unsupported' => false];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Große Datei hochladen, die nicht in einem Stück geht: zuerst als eine Datei in Stücken (Content-Range), sonst als Teile mit Abschlussdatei.
|
||||
* Die Unterstützung der NAS wird gemerkt („range_support“: 1 = geht, 0 = geht nicht), damit nicht jedes Mal vergeblich probiert wird.
|
||||
*/
|
||||
function nasbk_upload_big(array $cfg, string $localPath, string $name, int $chunkBytes): ?string
|
||||
{
|
||||
if ((string) plugin_setting(NASBK, 'range_support', '') !== '0') {
|
||||
$r = nasbk_upload_range($cfg, $localPath, $name, $chunkBytes);
|
||||
if ($r['error'] === null) { plugin_set_setting(NASBK, 'range_support', '1'); return null; }
|
||||
if (!$r['unsupported']) return $r['error'];
|
||||
plugin_set_setting(NASBK, 'range_support', '0');
|
||||
}
|
||||
return nasbk_upload_parts($cfg, $localPath, $name, $chunkBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Datei in Teilen hoch. Jedes Teil wird direkt aus der Backup-Datei gesendet (keine Zwischenkopie), danach auf der NAS auf die
|
||||
* richtige Größe geprüft und bei Fehlern bis zu dreimal neu gesendet. Erst wenn alle Teile da sind, kommt die Abschlussdatei „name.zip.parts“
|
||||
* (Größe, Prüfsumme SHA-256, Liste der Teile). Bei einem Fehler werden die schon hochgeladenen Teile entfernt.
|
||||
*
|
||||
* Lehnt der Server ein Teil als zu groß ab (HTTP 413), wird die Teilgröße halbiert (nicht unter 10 MB) und von vorn begonnen; die
|
||||
* funktionierende Größe wird gemerkt. So passt sich das Plugin einer unbekannten Grenze selbst an.
|
||||
*/
|
||||
function nasbk_upload_parts(array $cfg, string $localPath, string $name, int $partBytes): ?string
|
||||
{
|
||||
@set_time_limit(0);
|
||||
$min = NASBK_MIN_PART;
|
||||
$partBytes = max($min, $partBytes);
|
||||
while (true) {
|
||||
$res = nasbk_upload_parts_try($cfg, $localPath, $name, $partBytes);
|
||||
if ($res['error'] === null) return null;
|
||||
if ($res['code'] === 413 && $partBytes > $min) {
|
||||
$partBytes = max($min, intdiv($partBytes, 2));
|
||||
plugin_set_setting(NASBK, 'part_limit', (string) $partBytes); // künftig gleich mit dieser Größe beginnen
|
||||
continue;
|
||||
}
|
||||
return $res['error'];
|
||||
}
|
||||
}
|
||||
|
||||
/** Ein Durchgang mit fester Teilgröße. @return array{error:?string,code:int} */
|
||||
function nasbk_upload_parts_try(array $cfg, string $localPath, string $name, int $partBytes): array
|
||||
{
|
||||
clearstatcache(true, $localPath);
|
||||
$size = (int) filesize($localPath);
|
||||
$count = max(1, (int) ceil($size / $partBytes));
|
||||
$src = @fopen($localPath, 'rb');
|
||||
if ($src === false) return ['error' => 'Hochladen in Teilen: Die Backup-Datei konnte nicht gelesen werden.', 'code' => 0];
|
||||
$hash = hash_init('sha256');
|
||||
$done = [];
|
||||
$fail = function (string $msg, int $code) use (&$done, $cfg, $name, $src): array {
|
||||
fclose($src);
|
||||
foreach ($done as $n) nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, nasbk_part_name($name, $n)), ['timeout' => 60]);
|
||||
return ['error' => $msg, 'code' => $code];
|
||||
};
|
||||
$manifest = ['name' => $name, 'size' => $size, 'part_size' => $partBytes, 'parts' => []];
|
||||
for ($n = 1; $n <= $count; $n++) {
|
||||
$offset = ($n - 1) * $partBytes;
|
||||
$len = min($partBytes, $size - $offset);
|
||||
$url = nasbk_remote_url($cfg, nasbk_part_name($name, $n));
|
||||
$ok = false;
|
||||
$last = null;
|
||||
$lastCode = 0;
|
||||
for ($try = 1; $try <= 3 && !$ok; $try++) {
|
||||
$ctx = hash_copy($hash); // Prüfsumme nur übernehmen, wenn dieser Versuch gelingt (ein Wiederholungsversuch liest das Teil neu)
|
||||
$r = nasbk_request($cfg, 'PUT', $url, ['stream' => ['handle' => $src, 'offset' => $offset, 'length' => $len, 'hash' => $ctx], 'timeout' => max(300, (int) ceil($len / 1048576) * 20)]);
|
||||
$lastCode = $r['code'];
|
||||
if (in_array($r['code'], [200, 201, 204], true)) {
|
||||
$remote = nasbk_remote_size($cfg, $url);
|
||||
if ($remote === null || $remote === $len) { $ok = true; $hash = $ctx; }
|
||||
else $last = "Teil {$n} ist unvollständig auf der NAS angekommen ({$remote} statt {$len} Bytes).";
|
||||
} else {
|
||||
$last = nasbk_explain($r, "Hochladen von Teil {$n} von {$count} (" . round($len / 1048576, 1) . ' MB)');
|
||||
if (in_array($r['code'], [413, 401, 403, 507], true)) break; // wiederholen hilft nicht
|
||||
}
|
||||
if (!$ok && $try < 3) sleep(2 * $try);
|
||||
}
|
||||
if (!$ok) return $fail((string) $last, $lastCode);
|
||||
$done[] = $n;
|
||||
$manifest['parts'][] = ['n' => $n, 'file' => nasbk_part_name($name, $n), 'size' => $len];
|
||||
}
|
||||
fclose($src);
|
||||
$manifest['sha256'] = hash_final($hash);
|
||||
$manifest['created'] = date('c');
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'nasbkm');
|
||||
file_put_contents($tmp, json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
$r = nasbk_request($cfg, 'PUT', nasbk_remote_url($cfg, $name . '.parts'), ['file' => $tmp, 'timeout' => 120]);
|
||||
@unlink($tmp);
|
||||
if (!in_array($r['code'], [200, 201, 204], true)) {
|
||||
foreach ($done as $n) nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, nasbk_part_name($name, $n)), ['timeout' => 60]);
|
||||
return ['error' => nasbk_explain($r, 'Abschlussdatei schreiben'), 'code' => $r['code']];
|
||||
}
|
||||
return ['error' => null, 'code' => 201];
|
||||
}
|
||||
|
||||
/** Löscht eine Sicherung auf der NAS samt allen Teilen. $item stammt aus nasbk_list() (ohne Angabe wird die Liste geholt). */
|
||||
function nasbk_delete_remote(array $cfg, string $name, ?array $item = null): bool
|
||||
{
|
||||
if ($item === null) {
|
||||
$l = nasbk_list($cfg);
|
||||
foreach ($l['items'] as $it) if ($it['name'] === $name) $item = $it;
|
||||
}
|
||||
$okCodes = [200, 204, 404];
|
||||
$ok = true;
|
||||
if (!empty($item['parts'])) {
|
||||
// Zuerst die Abschlussdatei: Ab dann gilt die Sicherung nicht mehr als vollständig
|
||||
$r = nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, $name . '.parts'), ['timeout' => 60]);
|
||||
$ok = in_array($r['code'], $okCodes, true) && $ok;
|
||||
for ($n = 1; $n <= (int) $item['parts']; $n++) {
|
||||
$r = nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, nasbk_part_name($name, $n)), ['timeout' => 60]);
|
||||
$ok = in_array($r['code'], $okCodes, true) && $ok;
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
$r = nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, $name), ['timeout' => 60]);
|
||||
return in_array($r['code'], $okCodes, true);
|
||||
}
|
||||
|
||||
/** Löscht Sicherungen nach den beiden Regeln „älter als X Tage“ und „mehr als Y Stück“ (0 = Regel aus). Räumt außerdem abgebrochene Teil-Uploads auf. @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']];
|
||||
// Teile ohne Abschlussdatei, die älter als ein Tag sind (ein laufender Upload bleibt unberührt)
|
||||
foreach ($list['orphans'] as $o) {
|
||||
$t = strtotime($o['modified']) ?: 0;
|
||||
if ($t > 0 && time() - $t > 86400) nasbk_request($cfg, 'DELETE', nasbk_remote_url($cfg, $o['name']), ['timeout' => 60]);
|
||||
}
|
||||
if ($cfg['keep'] <= 0 && $cfg['keep_days'] <= 0) return ['deleted' => [], '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 (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']] = $it;
|
||||
}
|
||||
}
|
||||
if ($cfg['keep'] > 0) foreach (array_slice($items, $cfg['keep']) as $it) $drop[$it['name']] = true;
|
||||
if ($cfg['keep'] > 0) foreach (array_slice($items, $cfg['keep']) as $it) $drop[$it['name']] = $it;
|
||||
$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;
|
||||
foreach ($drop as $name => $it) {
|
||||
if (nasbk_delete_remote($cfg, $name, $it)) $deleted[] = $name;
|
||||
}
|
||||
return ['deleted' => $deleted, 'error' => ''];
|
||||
}
|
||||
|
||||
/** Hauptordner von DeskHub (Ort der Datei, die get_db() enthält: includes/db.php). */
|
||||
function nasbk_root(): string
|
||||
{
|
||||
@@ -330,13 +604,23 @@ function nasbk_make_archive(array $cfg): array
|
||||
$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)
|
||||
// Ältere DeskHub-Kerne (vor 1.0.45) schließen den Ordner backups/ bei einem anderen Zielordner nicht aus: dann steckten alle alten Backups
|
||||
// noch einmal im neuen. Dort schließt das Plugin ihn selbst aus (ab 1.0.45 übernimmt das der Kern und behält die .htaccess).
|
||||
if (!defined('DESKHUB_VERSION') || version_compare((string) DESKHUB_VERSION, '1.0.45', '<')) $options['exclude'] = array_values(array_unique(array_merge((array) $options['exclude'], ['backups'])));
|
||||
// „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'];
|
||||
// Nur ein Backup gleichzeitig (ab DeskHub 1.0.45): läuft gerade das automatische oder ein manuelles Backup, wird dieses hier später erneut versucht
|
||||
$locked = true;
|
||||
if (function_exists('backup_lock_acquire')) {
|
||||
$locked = backup_lock_acquire(get_db());
|
||||
if (!$locked) { nasbk_rmtree($tmp); return ['ok' => false, 'error' => 'Es läuft gerade ein anderes Backup. Das NAS-Backup wird später erneut versucht.']; }
|
||||
}
|
||||
$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 ($locked && function_exists('backup_lock_release')) backup_lock_release(get_db());
|
||||
}
|
||||
if (empty($res['ok'])) { nasbk_rmtree($tmp); return ['ok' => false, 'error' => (string) ($res['error'] ?? 'Backup fehlgeschlagen.')]; }
|
||||
if ($cfg['with_database']) {
|
||||
@@ -380,13 +664,18 @@ function nasbk_run(PDO $pdo, bool $manual = false): array
|
||||
$cfg = nasbk_config();
|
||||
if ($err = nasbk_config_error($cfg)) { nasbk_record(false, $err, $manual); return ['ok' => false, 'message' => $err]; }
|
||||
|
||||
$t0 = microtime(true);
|
||||
$arch = nasbk_make_archive($cfg);
|
||||
$tArchive = microtime(true) - $t0;
|
||||
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]; }
|
||||
$t1 = microtime(true);
|
||||
if ($err = nasbk_upload($cfg, $arch['path'], $arch['filename'])) { nasbk_record(false, $err, $manual); return ['ok' => false, 'message' => $err]; }
|
||||
$tUpload = microtime(true) - $t1;
|
||||
$rot = nasbk_rotate($cfg);
|
||||
$msg = $arch['filename'] . ' (' . $arch['size'] . ' MB, ' . ($cfg['mode'] === 'database' ? 'nur Datenbank' : 'komplett') . ') auf die NAS hochgeladen'
|
||||
. ' – Backup erstellen: ' . nasbk_duration($tArchive) . ', Hochladen: ' . nasbk_speed((int) round($arch['size'] * 1048576), $tUpload)
|
||||
. ($rot['deleted'] ? '; ' . count($rot['deleted']) . ' alte Sicherung(en) gelöscht' : '')
|
||||
. ($rot['error'] !== '' ? '; Aufbewahrung nicht ausgeführt: ' . $rot['error'] : '') . '.';
|
||||
nasbk_record(true, $msg, $manual);
|
||||
@@ -396,6 +685,21 @@ function nasbk_run(PDO $pdo, bool $manual = false): array
|
||||
}
|
||||
}
|
||||
|
||||
/** „2 Min 5 s“ bzw. „1 Std 3 Min“ für die Statusanzeige. */
|
||||
function nasbk_duration(float $sec): string
|
||||
{
|
||||
$sec = (int) round($sec);
|
||||
if ($sec < 60) return $sec . ' s';
|
||||
if ($sec < 3600) return intdiv($sec, 60) . ' Min ' . ($sec % 60) . ' s';
|
||||
return intdiv($sec, 3600) . ' Std ' . intdiv($sec % 3600, 60) . ' Min';
|
||||
}
|
||||
|
||||
/** „4550 MB in 1 Std 2 Min (1,2 MB/s)“: zeigt, wohin die Zeit geht. */
|
||||
function nasbk_speed(int $bytes, float $sec): string
|
||||
{
|
||||
$mb = $bytes / 1048576;
|
||||
return round($mb) . ' MB in ' . nasbk_duration($sec) . ($sec > 0 ? ' (' . number_format($mb / $sec, 1, ',', '') . ' MB/s)' : '');
|
||||
}
|
||||
/** Läuft gerade ein manuell gestartetes Backup? (Markierung verfällt nach 2 Stunden, falls ein Lauf abgebrochen wurde) */
|
||||
function nasbk_running(): bool
|
||||
{
|
||||
@@ -406,18 +710,39 @@ function nasbk_running(): bool
|
||||
/** Startet ein eigenes Backup nach dem Senden der Antwort (das Seitenladen wartet nicht auf das Backup). */
|
||||
function nasbk_run_background(PDO $pdo): void
|
||||
{
|
||||
nasbk_defer(fn() => nasbk_run($pdo, true), 'Backup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine lange Aufgabe (Backup, Upload, Download) nach dem Senden der Antwort aus. Wichtig: Die Sitzungssperre wird vorher freigegeben,
|
||||
* sonst wartet jede weitere Seite desselben Mitarbeiters, bis die Aufgabe fertig ist. Ohne fastcgi_finish_request wird die Ausgabe wenigstens
|
||||
* vollständig geschickt. Das Ergebnis steht danach im Status der Plugin-Seite.
|
||||
*
|
||||
* @return array{ok:bool,message:string} Sofortige Antwort für die Seite
|
||||
*/
|
||||
function nasbk_defer(callable $job, string $what): array
|
||||
{
|
||||
if (nasbk_running()) return ['ok' => false, 'message' => 'Es läuft bereits ein Vorgang (Backup, Upload oder Download). Bitte kurz warten.'];
|
||||
plugin_set_setting(NASBK, 'running_since', (string) time());
|
||||
register_shutdown_function(function () use ($pdo) {
|
||||
if (function_exists('fastcgi_finish_request')) @fastcgi_finish_request();
|
||||
register_shutdown_function(function () use ($job, $what) {
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
@fastcgi_finish_request();
|
||||
} else {
|
||||
while (ob_get_level() > 0) { @ob_end_flush(); }
|
||||
@flush();
|
||||
}
|
||||
if (session_status() === PHP_SESSION_ACTIVE) @session_write_close();
|
||||
@ignore_user_abort(true);
|
||||
@set_time_limit(0);
|
||||
try {
|
||||
nasbk_run($pdo, true);
|
||||
$job();
|
||||
} catch (Throwable $e) {
|
||||
nasbk_record(false, 'Backup abgebrochen: ' . $e->getMessage(), true);
|
||||
nasbk_record(false, $what . ' abgebrochen: ' . $e->getMessage(), true);
|
||||
} finally {
|
||||
plugin_set_setting(NASBK, 'running_since', '0');
|
||||
}
|
||||
});
|
||||
return ['ok' => true, 'message' => $what . ' läuft jetzt im Hintergrund. Das Ergebnis steht oben im Status; die Seite lädt sich von selbst neu.'];
|
||||
}
|
||||
/** 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
|
||||
@@ -484,10 +809,12 @@ 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];
|
||||
$t1 = microtime(true);
|
||||
if ($err = nasbk_upload($cfg, $path, $name)) return ['ok' => false, 'message' => $err];
|
||||
$took = microtime(true) - $t1;
|
||||
$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)' : '') . '.'];
|
||||
return ['ok' => true, 'message' => $name . ' auf die NAS kopiert (' . nasbk_speed((int) filesize($path), $took) . ')' . ($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). */
|
||||
@@ -544,10 +871,52 @@ function nasbk_save_settings(array $post, array $staff = []): array
|
||||
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))));
|
||||
plugin_set_setting(NASBK, 'split', in_array($post['split'] ?? 'auto', ['auto', 'always', 'never'], true) ? (string) $post['split'] : 'auto');
|
||||
plugin_set_setting(NASBK, 'part_mb', (string) max(10, min(4000, (int) ($post['part_mb'] ?? 200))));
|
||||
plugin_set_setting(NASBK, 'part_limit', '0');
|
||||
plugin_set_setting(NASBK, 'range_support', ''); // neu ausprobieren, ob die NAS Stück-Uploads kann
|
||||
plugin_set_setting(NASBK, 'single_limit', '0'); // gemerkte Größengrenze zurücksetzen: nach einer Änderung (z. B. am Proxy) neu ausprobieren
|
||||
nasbk_cache_clear();
|
||||
return ['ok' => true, 'message' => 'Einstellungen gespeichert.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine in Teilen abgelegte Sicherung herunter und setzt sie in $target zusammen. Geprüft werden Abschlussdatei, Größe jedes Teils,
|
||||
* Gesamtgröße und SHA-256 des Ganzen. Gibt eine Fehlermeldung oder null zurück.
|
||||
*/
|
||||
function nasbk_download_parts(array $cfg, string $name, string $target): ?string
|
||||
{
|
||||
@set_time_limit(0);
|
||||
$m = nasbk_request($cfg, 'GET', nasbk_remote_url($cfg, $name . '.parts'), ['timeout' => 60]);
|
||||
if ($m['code'] !== 200) return nasbk_explain($m, 'Abschlussdatei lesen');
|
||||
$man = json_decode($m['body'], true);
|
||||
if (!is_array($man) || ($man['name'] ?? '') !== $name || empty($man['parts']) || !is_array($man['parts']) || !preg_match('/^[a-f0-9]{64}$/', (string) ($man['sha256'] ?? ''))) {
|
||||
return 'Herunterladen: Die Abschlussdatei der Sicherung ist beschädigt.';
|
||||
}
|
||||
$out = @fopen($target, 'wb');
|
||||
if ($out === false) return 'Herunterladen: Die Zieldatei konnte nicht angelegt werden.';
|
||||
$piece = $target . '.teil';
|
||||
$total = 0;
|
||||
foreach ($man['parts'] as $p) {
|
||||
$n = (int) ($p['n'] ?? 0);
|
||||
$want = (int) ($p['size'] ?? -1);
|
||||
$pn = nasbk_part_name($name, $n);
|
||||
$r = nasbk_request($cfg, 'GET', nasbk_remote_url($cfg, $pn), ['save_to' => $piece, 'timeout' => 3600]);
|
||||
if ($r['code'] !== 200) { fclose($out); @unlink($piece); return nasbk_explain($r, "Herunterladen von Teil {$n}"); }
|
||||
if ($want < 0 || filesize($piece) !== $want) { fclose($out); @unlink($piece); return "Herunterladen: Teil {$n} ist unvollständig angekommen. Bitte erneut versuchen."; }
|
||||
$in = fopen($piece, 'rb');
|
||||
stream_copy_to_stream($in, $out);
|
||||
fclose($in);
|
||||
@unlink($piece);
|
||||
$total += $want;
|
||||
}
|
||||
fclose($out);
|
||||
clearstatcache(true, $target);
|
||||
if ($total !== (int) ($man['size'] ?? -1) || filesize($target) !== $total) return 'Herunterladen: Die zusammengesetzte Datei hat nicht die erwartete Größe.';
|
||||
if (!hash_equals((string) $man['sha256'], (string) hash_file('sha256', $target))) return 'Herunterladen: Die Prüfsumme der zusammengesetzten Datei stimmt nicht. Bitte erneut versuchen.';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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
|
||||
{
|
||||
@@ -559,10 +928,19 @@ function nasbk_fetch(array $cfg, string $name): array
|
||||
$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.']; }
|
||||
// Liegt die Sicherung in Teilen auf der NAS? Dann werden die Teile nacheinander geholt und zusammengesetzt.
|
||||
$item = null;
|
||||
$l = nasbk_list($cfg, 30);
|
||||
if ($l['ok']) foreach ($l['items'] as $it) if ($it['name'] === $name) $item = $it;
|
||||
if ($item && !empty($item['parts'])) {
|
||||
$err = nasbk_download_parts($cfg, $name, $tmp);
|
||||
if ($err !== null) { @unlink($tmp); return ['ok' => false, 'message' => $err]; }
|
||||
} else {
|
||||
$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'] ?? []))]; }
|
||||
@@ -662,26 +1040,31 @@ function nasbk_page_action(string $do, array $post, array $staff = []): array
|
||||
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.'];
|
||||
return nasbk_defer(fn() => nasbk_run(get_db(), true), 'Das eigene Backup');
|
||||
case 'sync':
|
||||
nasbk_cache_clear();
|
||||
$r = nasbk_sync_local(get_db(), true);
|
||||
return ['ok' => $r['ok'], 'message' => $r['message']];
|
||||
return nasbk_defer(function () {
|
||||
nasbk_cache_clear();
|
||||
$r = nasbk_sync_local(get_db(), true);
|
||||
if ($r['ok'] && $r['done'] === 0) nasbk_record(true, $r['message'] !== '' ? $r['message'] : 'Es gab nichts zu kopieren.', true);
|
||||
}, 'Das Kopieren der Backups');
|
||||
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;
|
||||
return nasbk_defer(function () use ($cfg, $path, $name) {
|
||||
$r = nasbk_push_file($cfg, $path, $name);
|
||||
nasbk_record($r['ok'], $r['message'], true);
|
||||
}, 'Das Hochladen von ' . $name);
|
||||
case 'fetch':
|
||||
return nasbk_fetch($cfg, $name);
|
||||
if (!nasbk_valid_name($name)) return ['ok' => false, 'message' => 'Ungültiger Dateiname.'];
|
||||
return nasbk_defer(function () use ($cfg, $name) {
|
||||
$r = nasbk_fetch($cfg, $name);
|
||||
nasbk_record($r['ok'], $r['message'], true);
|
||||
}, 'Das Herunterladen von ' . $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]);
|
||||
$gone = nasbk_delete_remote($cfg, $name);
|
||||
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 $gone ? ['ok' => true, 'message' => $name . ' von der NAS gelöscht.'] : ['ok' => false, 'message' => 'Löschen: Die Sicherung (oder einzelne Teile) ließ sich auf der NAS nicht löschen. Prüfe, ob der Benutzer dort Löschrechte hat.'];
|
||||
}
|
||||
return ['ok' => false, 'message' => 'Unbekannte Aktion.'];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$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'];
|
||||
// Post/Redirect/Get: Nach einem Klick auf die Seite selbst weiterleiten. Sonst schickt das automatische Neuladen (alle 10 Sekunden, solange ein
|
||||
// Vorgang läuft) oder F5 den Klick noch einmal ab: „läuft bereits“ oder, sobald der erste fertig ist, ein ZWEITES Backup bzw. ein zweiter Upload.
|
||||
// Plugin-Seiten werden erst nach der Kopfzeile des Layouts ausgegeben, eine Weiterleitung per Header ist dann nicht mehr möglich:
|
||||
// die Seite ersetzt sich deshalb selbst durch die normale (GET-)Adresse. Die Meldung zeigt der Kern oben auf der nächsten Seite.
|
||||
flash($r['ok'] ? 'success' : 'error', $r['message']);
|
||||
$to = nasbk_page_url();
|
||||
echo '<p class="nb-hint">Einen Moment …</p><script>location.replace(' . json_encode($to) . ');</script><noscript><meta http-equiv="refresh" content="0;url=' . e($to) . '"><a href="' . e($to) . '">Weiter</a></noscript>';
|
||||
return;
|
||||
}
|
||||
$cfg = nasbk_config();
|
||||
$hasPassword = $cfg['password'] !== '';
|
||||
@@ -177,6 +184,21 @@ $checkMap = is_array($checkMap) ? $checkMap : [];
|
||||
<div class="nb-row"><label class="nb-l">Aktuell aktive Regeln</label><div id="nb-rules"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card nb-card">
|
||||
<h3>📦 Große Backups in Stücken hochladen</h3>
|
||||
<p class="nb-hint" style="margin:0 0 6px">Manche Server oder Proxys vor der NAS nehmen nur Dateien bis zu einer Größe an (zum Beispiel Cloudflare: 100 MB, Fehler „HTTP 413“). Dann wird das Backup in Stücken hochgeladen. <strong>Bevorzugt als eine einzige Datei</strong>: Jedes Stück sagt der NAS per <code>Content-Range</code>, wohin es gehört (kann z. B. Synology DSM 7), und auf der NAS liegt am Ende direkt die fertige <code>.zip</code>. Nach jedem Stück wird die Größe geprüft. Kann die NAS das nicht, lädt das Plugin ersatzweise Teile (<code>backup_….zip.part001</code> …) hoch und legt erst danach eine kleine Abschlussdatei an; beim Zurückholen setzt es sie wieder zusammen und prüft die Prüfsumme.</p>
|
||||
<div class="nb-row"><label class="nb-l" for="split">Wann in Stücken?</label>
|
||||
<div><select id="split" name="split" style="max-width:360px">
|
||||
<option value="auto" <?= $cfg['split'] === 'auto' ? 'selected' : '' ?>>Automatisch (erst ganz, bei Fehler in Stücken)</option>
|
||||
<option value="always" <?= $cfg['split'] === 'always' ? 'selected' : '' ?>>Immer, wenn größer als die Stückgröße</option>
|
||||
<option value="never" <?= $cfg['split'] === 'never' ? 'selected' : '' ?>>Nie (immer als eine Datei)</option>
|
||||
</select>
|
||||
<p class="nb-hint">„Automatisch“ lässt kleine und mittlere Backups als normale <code>.zip</code> auf der NAS. Scheitert der Upload an einer Größengrenze, lädt das Plugin die Datei in Stücken hoch und merkt sich die Größe, damit solche Backups künftig gleich in Stücken gehen.</p></div></div>
|
||||
<div class="nb-row"><label class="nb-l" for="part_mb">Stückgröße</label>
|
||||
<div><input type="number" id="part_mb" name="part_mb" min="10" max="4000" value="<?= (int) $cfg['part_mb'] ?>" style="max-width:110px"> MB
|
||||
<p class="nb-hint">Größe eines Stücks, zwischen 10 und 4000 MB (Standard 200 MB). Die Stücke werden direkt aus dem Backup gesendet (keine Zwischenkopie). Die Gesamtdauer hängt vor allem von der Upload-Geschwindigkeit ab; größere Stücke sparen nur wenige Anfragen, kleinere verlieren bei einem Verbindungsabbruch weniger. Lehnt der Server ein Stück als zu groß ab (HTTP 413), halbiert das Plugin die Größe selbst, bis es klappt, und merkt sich den Wert. Hinter Cloudflare (Gratis-Tarif) sind höchstens 100 MB je Stück möglich, dort 50 MB einstellen.</p></div></div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row"><button type="submit" class="btn">Einstellungen speichern</button></div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"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.",
|
||||
"version": "2.6.3",
|
||||
"description": "Kopiert die Backups des Servers (Zeitplan und Knopf der Seite Backup) automatisch per WebDAV auf eine Synology NAS oder einen anderen WebDAV-Server. Große Backups gehen in Stücken als eine einzige Datei (Content-Range), ersatzweise als Teile. 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"]
|
||||
|
||||
Reference in New Issue
Block a user