Upload via GUI (97 Dateien)
This commit is contained in:
@@ -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.'];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user