prepare('SELECT value FROM backup_settings WHERE key_name=?'); $s->execute([$key]); $r = $s->fetch(); return $r ? $r['value'] : null; } /** * Führt das automatische Backup aus (falls laut Zeitplan fällig, oder immer bei $force=true). * Kann vom Cron und manuell aus settings.php aufgerufen werden. * * @return array{ran: bool, ok: bool, log: string[]} */ function run_process_backups(PDO $pdo, bool $verbose = false, bool $force = false): array { $log = []; // Zeitplan prüfen $schedule = bset($pdo, 'schedule') ?: 'disabled'; $lastBackup = bset($pdo, 'last_auto_backup') ?: '1970-01-01'; $now = time(); $shouldRun = false; switch ($schedule) { case 'daily': $shouldRun = (strtotime($lastBackup) < strtotime('-1 day', $now)); break; case 'weekly': $shouldRun = (strtotime($lastBackup) < strtotime('-7 days', $now)); break; case 'monthly': $shouldRun = (strtotime($lastBackup) < strtotime('-30 days', $now)); break; default: $shouldRun = false; // disabled } if ($force) { $shouldRun = true; } if (!$shouldRun) { if ($schedule === 'disabled' && !$force) { $log[] = date('Y-m-d H:i:s') . ' – Automatische Backups sind deaktiviert (Zeitplan: disabled).'; } else { $log[] = date('Y-m-d H:i:s') . " – Kein Backup nötig (Zeitplan: $schedule, letztes: $lastBackup)"; } return ['ran' => false, 'ok' => true, 'log' => $log]; } // Nur ein Backup gleichzeitig: Der echte Cronjob, der eingebaute Hintergrunddienst und der Knopf auf der Seite „Backup“ dürfen sich nicht // überlappen. „Letztes Backup“ wird erst NACH dem Erstellen eingetragen; ein zweiter Lauf würde das Backup sonst weiterhin als fällig sehen. if (!backup_lock_acquire($pdo)) { $log[] = date('Y-m-d H:i:s') . ' – Es läuft bereits ein Backup, dieser Lauf wird übersprungen.'; return ['ran' => false, 'ok' => true, 'log' => $log]; } if (!$force) { // Ein anderer Lauf kann gerade erst fertig geworden sein: nach dem Erhalt der Sperre noch einmal prüfen $lastNow = bset($pdo, 'last_auto_backup') ?: '1970-01-01'; $days = ['daily' => 1, 'weekly' => 7, 'monthly' => 30][$schedule] ?? null; if ($days === null || strtotime($lastNow) >= strtotime("-{$days} days", time())) { backup_lock_release($pdo); $log[] = date('Y-m-d H:i:s') . ' – Kein Backup nötig (ein anderer Lauf war gerade schneller).'; return ['ran' => false, 'ok' => true, 'log' => $log]; } } $log[] = date('Y-m-d H:i:s') . ' – Starte Backup...'; try { $result = create_backup(); if (!$result['ok']) { $log[] = date('Y-m-d H:i:s') . " – FEHLER: {$result['error']}"; return ['ran' => true, 'ok' => false, 'log' => $log]; } $log[] = date('Y-m-d H:i:s') . " – Backup erstellt: {$result['filename']} ({$result['size']} MB)"; // Sofort prüfen, ob sich das Backup wiederherstellen lässt (ohne etwas zu verändern) require_once __DIR__ . '/../includes/backup_restore.php'; $check = $result['verify'] ?? backup_verify_and_record($result['path']); // create_backup() prüft jedes Backup automatisch $log[] = date('Y-m-d H:i:s') . ($check['ok'] ? ' – Prüfung: Backup ist vollständig und wiederherstellbar.' : ' – WARNUNG, Backup nicht wiederherstellbar: ' . implode(' ', $check['errors'])); // Letztes Backup-Datum speichern $backupTime = date('Y-m-d H:i:s'); $backupSettingSql = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite' ? "INSERT INTO backup_settings (key_name, value, updated_at) VALUES ('last_auto_backup', ?, ?) ON CONFLICT(key_name) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at" : "INSERT INTO backup_settings (key_name, value, updated_at) VALUES ('last_auto_backup', ?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value), updated_at=VALUES(updated_at)"; $pdo->prepare($backupSettingSql)->execute([$backupTime, $backupTime]); } finally { backup_lock_release($pdo); // auch bei Fehlern und vorzeitigem Abbruch freigeben } // ── FTP-Upload (optional) ──────────────────────────────────────────────────── $ftpHost = bset($pdo, 'ftp_host'); $ftpUser = bset($pdo, 'ftp_user'); $ftpPass = bset($pdo, 'ftp_pass'); $ftpPath = bset($pdo, 'ftp_path') ?: '/'; $ftpPort = (int)(bset($pdo, 'ftp_port') ?: 21); $ftpTls = bset($pdo, 'ftp_tls') === '1'; if ($ftpHost && $ftpUser) { $log[] = date('Y-m-d H:i:s') . " – FTP-Upload nach $ftpHost..."; if (!function_exists('ftp_connect')) { $log[] = date('Y-m-d H:i:s') . ' – WARNUNG: PHP FTP-Erweiterung nicht verfügbar.'; } else { $ftp = $ftpTls ? @ftp_ssl_connect($ftpHost, $ftpPort, 10) : @ftp_connect($ftpHost, $ftpPort, 10); if ($ftp && @ftp_login($ftp, $ftpUser, $ftpPass)) { ftp_pasv($ftp, true); $remotePath = rtrim($ftpPath, '/') . '/' . $result['filename']; if (ftp_put($ftp, $remotePath, $result['path'], FTP_BINARY)) { $log[] = date('Y-m-d H:i:s') . " – FTP-Upload erfolgreich: $remotePath"; // FTP-Rotation $ftpKeep = (int)(bset($pdo, 'ftp_keep') ?: 5); $files = @ftp_nlist($ftp, $ftpPath); if ($files) { $backupFiles = array_filter($files, fn($f) => str_contains($f, 'backup_')); rsort($backupFiles); foreach (array_slice($backupFiles, $ftpKeep) as $old) { @ftp_delete($ftp, $old); $log[] = date('Y-m-d H:i:s') . " – FTP-Rotation: $old gelöscht"; } } } else { $log[] = date('Y-m-d H:i:s') . ' – FTP-Upload fehlgeschlagen.'; } ftp_close($ftp); } else { $log[] = date('Y-m-d H:i:s') . ' – FTP-Verbindung fehlgeschlagen.'; } } } // ── Lokale Rotation ────────────────────────────────────────────────────────── $keepLocal = (int)(bset($pdo, 'rotate_keep') ?: 0); if ($keepLocal > 0) { $backupDir = realpath(__DIR__ . '/../backups'); $files = array_merge( glob($backupDir . '/backup_*.zip') ?: [], glob($backupDir . '/backup_*.tar.gz') ?: [] ); usort($files, fn($a, $b) => filemtime($b) - filemtime($a)); foreach (array_slice($files, $keepLocal) as $old) { unlink($old); $log[] = date('Y-m-d H:i:s') . ' – Rotation: ' . basename($old) . ' gelöscht'; } } // Noch nicht oder lange nicht geprüfte Backups nachholen (zum Beispiel ältere oder von der NAS geholte) $late = backup_verify_pending(3); if ($late['checked']) $log[] = date('Y-m-d H:i:s') . ' – ' . $late['checked'] . ' weitere(s) Backup(s) geprüft' . ($late['bad'] ? ', davon ' . $late['bad'] . ' NICHT wiederherstellbar' : '') . '.'; set_setting('cron_process_backups_last_run', date('Y-m-d H:i:s')); $log[] = date('Y-m-d H:i:s') . ' – Fertig.'; return ['ran' => true, 'ok' => true, 'log' => $log]; } if (php_sapi_name() === 'cli') { $result = run_process_backups(get_db(), true, false); foreach ($result['log'] as $line) { echo $line . "\n"; } if (!$result['ok']) { exit(1); } }