#!/usr/bin/env php > /var/log/xamxam-backup-$(date +\%Y-\%m-\%d).log 2>&1 * * Exit codes: 0 ok (or alert sent), 1 error. */ declare(strict_types=1); // Resolve APP_ROOT robustly — same convention as the other CLI scripts here. $prodRoot = '/var/www/xamxam'; if (is_dir($prodRoot . '/src') && is_file($prodRoot . '/src/Database.php')) { define('APP_ROOT', $prodRoot); } else { define('APP_ROOT', dirname(__DIR__) . '/app'); } const BACKUP_DIR = '/var/backups/xamxam'; const STATE_FILE = BACKUP_DIR . '/.last_backup_alert'; const ALERT_EMAIL = 'xamxam@erg.be'; const WEBDAV_BASE = 'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK'; const REMOTE_PREFIX = 'xamxam-db-'; const REMOTE_STALE_SECONDS = 172800; // 48h — daily sync, allow one missed day // Autoload dependencies (PHPMailer, etc.) from composer. $autoloads = [ APP_ROOT . '/../vendor/autoload.php', // repo root / vendor APP_ROOT . '/vendor/autoload.php', // app / vendor ]; foreach ($autoloads as $a) { if (is_file($a)) { require_once $a; break; } } // Read-only probe: do NOT run schema migrations against the live DB just to // check backup freshness. If DatabaseMigrations isn't loaded, no-op it so the // Database constructor only opens a PDO connection. if (!class_exists('DatabaseMigrations', false)) { class DatabaseMigrations { public function run(): void {} } } require_once APP_ROOT . '/src/Crypto.php'; require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/SmtpRelay.php'; // Staleness threshold: default 2 hours (matches the admin system-page status). $staleAfterSeconds = (int) (getenv('STALE_AFTER_SECONDS') ?: 7200); if ($staleAfterSeconds < 60) { $staleAfterSeconds = 7200; } $newestMtime = 0; $newestName = null; $files = glob(BACKUP_DIR . '/db-*.db.gz') ?: []; foreach ($files as $f) { $m = @filemtime($f); if ($m !== false && $m > $newestMtime) { $newestMtime = $m; $newestName = $f; } } $now = time(); $age = $newestMtime > 0 ? ($now - $newestMtime) : null; $localDown = ($age === null) || ($age > $staleAfterSeconds); // Remote (Nextcloud) staleness — only relevant if we have credentials. // PROPFIND the backup folder and find the newest snapshot's last-modified time. $remoteDown = false; $remoteStatus = ''; try { $db = new Database(); $smtp = SmtpRelay::getSettings($db); } catch (Throwable $e) { // Cannot read credentials — skip the remote check rather than fail the run. $db = null; $smtp = null; } if ($db !== null && !empty($smtp['username']) && !empty($smtp['password'])) { $remote = remoteFreshness($smtp['username'], $smtp['password']); if ($remote['oldest_age'] === null) { $remoteDown = true; $remoteStatus = 'aucune copie distante trouvée dans XAMXAM-BCK'; } elseif ($remote['oldest_age'] > REMOTE_STALE_SECONDS) { $remoteDown = true; $hours = (int) round($remote['oldest_age'] / 3600.0); $remoteStatus = "la copie Nextcloud la plus récente date d'il y a {$hours} h"; } } else { // No creds — can't check; don't alarm about remote. $remoteDown = false; } $isDown = $localDown || $remoteDown; if (!$isDown) { // Backups are fresh. Clear any prior alert marker so a future outage // triggers a fresh email. if (is_file(STATE_FILE)) { @unlink(STATE_FILE); } exit(0); } // Stale/absent — avoid flooding: only email once per incident. if (is_file(STATE_FILE)) { exit(0); } // Compose the alert. $issues = []; if ($localDown) { if ($age === null) { $issues[] = 'aucune sauvegarde locale trouvée dans ' . BACKUP_DIR; } else { $hours = (int) round($age / 3600.0); $issues[] = "sauvegarde locale la plus récente il y a {$hours} h" . ($newestName ? ' (' . basename($newestName) . ')' : ''); } } if ($remoteDown) { $issues[] = $remoteStatus; } $status = implode(' ; ', $issues); $subject = '⚠ XAMXAM — sauvegarde SQLite inactive'; $remoteNote = $remoteDown ? "\n
Copie distante (Nextcloud) : en retard — {$remoteStatus}.
" : ''; $body = <<Le système de sauvegarde XAMXAM semble en panne : {$status}.
{$remoteNote}Veuillez vérifier le cron de sauvegarde et les journaux :
tail -50 /var/log/xamxam-backup-$(date +%Y-%m-%d).log ls -lth /var/backups/xamxam/
Cet e-mail est envoyé automatiquement par le moniteur de sauvegarde XAMXAM.