Files
deskhub/DeskHub-Webseite/cron/process_backups.php
T
2026-10-25 17:47:16 +00:00

182 lines
8.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Direkter Web-Zugriff (Browser/URL) wird geblockt. Ausnahmen: echter CLI-Cronjob
// sowie Einbindung per require_once aus dem authentifizierten Backend (staff/settings.php),
// das vor dem require_once die Konstante DESKHUB_CRON_RUNNER definiert.
if (php_sapi_name() !== 'cli' && !defined('DESKHUB_CRON_RUNNER')) { http_response_code(403); exit('Kein Web-Zugriff.'); }
/**
* Automatischer Backup-Cron
* Crontab-Beispiele:
* Täglich 02:00: 0 2 * * * php /pfad/zu/deskhub/cron/process_backups.php
* Wöchentlich: 0 2 * * 1 php /pfad/zu/deskhub/cron/process_backups.php
* Monatlich: 0 2 1 * * php /pfad/zu/deskhub/cron/process_backups.php
*
* Kann außerdem manuell aus dem Backend (Einstellungen → Cron-Jobs) ausgeführt werden
* (dort wird der Zeitplan ignoriert und immer sofort ein Backup erstellt, $force=true).
*/
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/backup.php';
function bset(PDO $pdo, string $key): ?string {
$s = $pdo->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);
}
}