fix(ha): try/finally & purge temp /tmp cluster (v2.5.2)
This commit is contained in:
1 parent
c96af67ac9
commit
8df103d7a0
3 files changed
+203
-173
No files matched your search
@@ -81,6 +81,9 @@ fi
|
||||
|
||||
echo -e "\n${C_YELLOW}-> Mise à jour vers la version v${REMOTE_VER}...${C_RESET}"
|
||||
|
||||
# Nettoyage préventif d'éventuels anciens dossiers temporaires HA dans /tmp
|
||||
find /tmp -maxdepth 1 \( -name 'nexus_cluster_extract_*' -o -name 'nexus_cluster_tar_*' -o -name 'nexus_cluster_recv_*' -o -name 'nexus_cluster_sync_*' -o -name 'nexus_cluster_db_*' \) -mmin +10 -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# 1. Sauvegarde préventive automatique (Disaster Recovery complète)
|
||||
echo -e "${C_GRAY}[1/6] Création d'une sauvegarde préventive du système (Disaster Recovery)...${C_RESET}"
|
||||
php -r '
|
||||
|
||||
+198
-172
@@ -495,101 +495,98 @@ class ClusterManager
|
||||
throw new RuntimeException("Fichier de base de données introuvable : {$dbPath}");
|
||||
}
|
||||
|
||||
$this->cleanupStaleClusterTempFiles();
|
||||
$uniq = getmypid() . '_' . bin2hex(random_bytes(4));
|
||||
|
||||
// Snapshot 100% atomique de la base NexusPBX via VACUUM INTO / .backup officiel SQLite
|
||||
$tmpDbDump = '/tmp/nexus_cluster_db_' . $uniq . '.sqlite';
|
||||
@unlink($tmpDbDump);
|
||||
try {
|
||||
$this->db->exec("VACUUM INTO " . $this->db->quote($tmpDbDump));
|
||||
} catch (\Throwable $e) {
|
||||
shell_exec("sqlite3 " . escapeshellarg($dbPath) . " \".backup " . escapeshellarg($tmpDbDump) . "\" 2>/dev/null || true");
|
||||
}
|
||||
|
||||
if (!file_exists($tmpDbDump) || filesize($tmpDbDump) === 0) {
|
||||
$this->db->exec('PRAGMA wal_checkpoint(FULL);');
|
||||
@copy($dbPath, $tmpDbDump);
|
||||
}
|
||||
|
||||
$dbIntegrity = $this->checkSqliteIntegrity($tmpDbDump);
|
||||
if ($dbIntegrity !== 'ok') {
|
||||
@unlink($tmpDbDump);
|
||||
throw new RuntimeException("Échec de vérification d'intégrité de la base locale avant réplication : {$dbIntegrity}");
|
||||
}
|
||||
|
||||
$tmpDbDump = '/tmp/nexus_cluster_db_' . $uniq . '.sqlite';
|
||||
$tmpArchive = '/tmp/nexus_cluster_sync_' . $uniq . '.tar.gz';
|
||||
$tmpAstdb = '/tmp/astdb_cluster_sync_' . $uniq . '.sqlite3';
|
||||
$hasAstdb = false;
|
||||
$tmpAstdb = '/tmp/astdb_cluster_sync_' . $uniq . '.sqlite3';
|
||||
$tmpDbDir = '/tmp/nexus_cluster_tar_' . $uniq;
|
||||
$hasAstdb = false;
|
||||
|
||||
// Sauvegarde intègre et atomique de la base de contacts Asterisk (astdb.sqlite3)
|
||||
$astdbPath = '/var/lib/asterisk/astdb.sqlite3';
|
||||
if (file_exists($astdbPath) && is_readable($astdbPath)) {
|
||||
@unlink($tmpAstdb);
|
||||
try {
|
||||
@unlink($tmpDbDump);
|
||||
try {
|
||||
if (class_exists(\SQLite3::class)) {
|
||||
$srcAst = new \SQLite3($astdbPath, SQLITE3_OPEN_READONLY);
|
||||
$dstAst = new \SQLite3($tmpAstdb);
|
||||
$srcAst->backup($dstAst);
|
||||
$srcAst->close();
|
||||
$dstAst->close();
|
||||
} else {
|
||||
shell_exec("sqlite3 " . escapeshellarg($astdbPath) . " \".backup " . escapeshellarg($tmpAstdb) . "\" 2>/dev/null || cp -f " . escapeshellarg($astdbPath) . " " . escapeshellarg($tmpAstdb) . " 2>/dev/null || true");
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
@copy($astdbPath, $tmpAstdb);
|
||||
$this->db->exec("VACUUM INTO " . $this->db->quote($tmpDbDump));
|
||||
} catch (\Throwable $e) {
|
||||
shell_exec("sqlite3 " . escapeshellarg($dbPath) . " \".backup " . escapeshellarg($tmpDbDump) . "\" 2>/dev/null || true");
|
||||
}
|
||||
if (file_exists($tmpAstdb) && filesize($tmpAstdb) > 0) {
|
||||
$integrity = $this->checkSqliteIntegrity($tmpAstdb);
|
||||
if ($integrity === 'ok') {
|
||||
// Purger les souscriptions persistantes PJSIP de l'archive (incompatibles entre instances Asterisk)
|
||||
try {
|
||||
$astPdo = new PDO('sqlite:' . $tmpAstdb);
|
||||
$astPdo->exec("DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%'");
|
||||
$astPdo = null;
|
||||
} catch (\Throwable) {
|
||||
shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " \"DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%';\" 2>/dev/null || true");
|
||||
|
||||
if (!file_exists($tmpDbDump) || filesize($tmpDbDump) === 0) {
|
||||
$this->db->exec('PRAGMA wal_checkpoint(FULL);');
|
||||
@copy($dbPath, $tmpDbDump);
|
||||
}
|
||||
|
||||
$dbIntegrity = $this->checkSqliteIntegrity($tmpDbDump);
|
||||
if ($dbIntegrity !== 'ok') {
|
||||
throw new RuntimeException("Échec de vérification d'intégrité de la base locale avant réplication : {$dbIntegrity}");
|
||||
}
|
||||
|
||||
// Sauvegarde intègre et atomique de la base de contacts Asterisk (astdb.sqlite3)
|
||||
$astdbPath = '/var/lib/asterisk/astdb.sqlite3';
|
||||
if (file_exists($astdbPath) && is_readable($astdbPath)) {
|
||||
@unlink($tmpAstdb);
|
||||
try {
|
||||
if (class_exists(\SQLite3::class)) {
|
||||
$srcAst = new \SQLite3($astdbPath, SQLITE3_OPEN_READONLY);
|
||||
$dstAst = new \SQLite3($tmpAstdb);
|
||||
$srcAst->backup($dstAst);
|
||||
$srcAst->close();
|
||||
$dstAst->close();
|
||||
} else {
|
||||
shell_exec("sqlite3 " . escapeshellarg($astdbPath) . " \".backup " . escapeshellarg($tmpAstdb) . "\" 2>/dev/null || cp -f " . escapeshellarg($astdbPath) . " " . escapeshellarg($tmpAstdb) . " 2>/dev/null || true");
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
@copy($astdbPath, $tmpAstdb);
|
||||
}
|
||||
if (file_exists($tmpAstdb) && filesize($tmpAstdb) > 0) {
|
||||
$integrity = $this->checkSqliteIntegrity($tmpAstdb);
|
||||
if ($integrity === 'ok') {
|
||||
// Purger les souscriptions persistantes PJSIP de l'archive (incompatibles entre instances Asterisk)
|
||||
try {
|
||||
$astPdo = new PDO('sqlite:' . $tmpAstdb);
|
||||
$astPdo->exec("DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%'");
|
||||
$astPdo = null;
|
||||
} catch (\Throwable) {
|
||||
shell_exec("sqlite3 " . escapeshellarg($tmpAstdb) . " \"DELETE FROM astdb WHERE key LIKE '/subscription_persistence/%';\" 2>/dev/null || true");
|
||||
}
|
||||
$hasAstdb = true;
|
||||
}
|
||||
$hasAstdb = true;
|
||||
} else {
|
||||
@unlink($tmpAstdb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Préparation du dossier temporaire de tarage pour isoler database.sqlite
|
||||
$tmpDbDir = '/tmp/nexus_cluster_tar_' . $uniq;
|
||||
@mkdir($tmpDbDir, 0755, true);
|
||||
rename($tmpDbDump, $tmpDbDir . '/database.sqlite');
|
||||
// Préparation du dossier temporaire de tarage pour isoler database.sqlite
|
||||
@mkdir($tmpDbDir, 0755, true);
|
||||
rename($tmpDbDump, $tmpDbDir . '/database.sqlite');
|
||||
|
||||
$cmd = "tar -czf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpDbDir) . " database.sqlite";
|
||||
if (is_dir('/var/lib/asterisk/sounds/custom')) {
|
||||
$cmd .= " -C /var/lib/asterisk/sounds custom";
|
||||
}
|
||||
if (is_dir('/var/spool/asterisk/voicemail')) {
|
||||
$cmd .= " -C /var/spool/asterisk voicemail";
|
||||
}
|
||||
if (is_dir('/var/www/nexus-pbx/public/avatars')) {
|
||||
$cmd .= " -C /var/www/nexus-pbx/public avatars";
|
||||
}
|
||||
if ($hasAstdb) {
|
||||
$cmd .= " -C /tmp " . escapeshellarg(basename($tmpAstdb));
|
||||
}
|
||||
exec($cmd . " 2>&1", $tarOut, $tarCode);
|
||||
$cmd = "tar -czf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpDbDir) . " database.sqlite";
|
||||
if (is_dir('/var/lib/asterisk/sounds/custom')) {
|
||||
$cmd .= " -C /var/lib/asterisk/sounds custom";
|
||||
}
|
||||
if (is_dir('/var/spool/asterisk/voicemail')) {
|
||||
$cmd .= " -C /var/spool/asterisk voicemail";
|
||||
}
|
||||
if (is_dir('/var/www/nexus-pbx/public/avatars')) {
|
||||
$cmd .= " -C /var/www/nexus-pbx/public avatars";
|
||||
}
|
||||
if ($hasAstdb) {
|
||||
$cmd .= " -C /tmp " . escapeshellarg(basename($tmpAstdb));
|
||||
}
|
||||
exec($cmd . " 2>&1", $tarOut, $tarCode);
|
||||
|
||||
@unlink($tmpDbDir . '/database.sqlite');
|
||||
@rmdir($tmpDbDir);
|
||||
if ($hasAstdb) {
|
||||
if (!file_exists($tmpArchive) || filesize($tmpArchive) === 0 || $tarCode >= 2) {
|
||||
throw new RuntimeException("Échec de génération de l'archive de réplication (tar code {$tarCode}).");
|
||||
}
|
||||
|
||||
$archiveData = file_get_contents($tmpArchive);
|
||||
} finally {
|
||||
@unlink($tmpDbDump);
|
||||
@unlink($tmpAstdb);
|
||||
}
|
||||
|
||||
if (!file_exists($tmpArchive) || filesize($tmpArchive) === 0 || $tarCode >= 2) {
|
||||
@unlink($tmpArchive);
|
||||
throw new RuntimeException("Échec de génération de l'archive de réplication (tar code {$tarCode}).");
|
||||
shell_exec('rm -rf ' . escapeshellarg($tmpDbDir) . ' 2>/dev/null || true');
|
||||
}
|
||||
|
||||
$archiveData = file_get_contents($tmpArchive);
|
||||
@unlink($tmpArchive);
|
||||
|
||||
// Collecte des adresses IP bannies dans Fail2ban (jails actives)
|
||||
$bannedIps = $this->getActiveFail2banBans();
|
||||
|
||||
@@ -694,110 +691,114 @@ class ClusterManager
|
||||
throw new RuntimeException("Paquet de réplication vide ou corrompu.");
|
||||
}
|
||||
|
||||
$uniqRecv = getmypid() . '_' . bin2hex(random_bytes(4));
|
||||
$archiveRaw = base64_decode($data['archive_b64']);
|
||||
$tmpArchive = '/tmp/nexus_cluster_recv_' . $uniqRecv . '.tar.gz';
|
||||
file_put_contents($tmpArchive, $archiveRaw);
|
||||
$this->cleanupStaleClusterTempFiles();
|
||||
$uniqRecv = getmypid() . '_' . bin2hex(random_bytes(4));
|
||||
$archiveRaw = base64_decode($data['archive_b64'], true);
|
||||
if ($archiveRaw === false || $archiveRaw === '') {
|
||||
throw new RuntimeException("Archive de réplication invalide (décodage base64).");
|
||||
}
|
||||
|
||||
// Extraction sécurisée dans un dossier temporaire pour validation préalable
|
||||
$tmpArchive = '/tmp/nexus_cluster_recv_' . $uniqRecv . '.tar.gz';
|
||||
$tmpExtractDir = '/tmp/nexus_cluster_extract_' . $uniqRecv;
|
||||
@mkdir($tmpExtractDir, 0755, true);
|
||||
shell_exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpExtractDir) . " 2>/dev/null || true");
|
||||
|
||||
$extractedDb = $tmpExtractDir . '/database.sqlite';
|
||||
if (!file_exists($extractedDb) || filesize($extractedDb) === 0) {
|
||||
@unlink($tmpArchive);
|
||||
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
|
||||
throw new RuntimeException("L'archive de réplication reçue ne contient pas de base de données valide.");
|
||||
}
|
||||
|
||||
// Contrôle d'intégrité SQLite strict du fichier reçu AVANT toute modification du système local
|
||||
$check = $this->checkSqliteIntegrity($extractedDb);
|
||||
if ($check !== 'ok') {
|
||||
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
|
||||
@unlink($tmpArchive);
|
||||
throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application.");
|
||||
}
|
||||
|
||||
// Sauvegarde d'urgence de la base actuelle avant synchronisation
|
||||
$dbPath = Database::getDatabasePath();
|
||||
if (file_exists($dbPath)) {
|
||||
@copy($dbPath, $dbPath . '.pre_cluster_sync.bak');
|
||||
}
|
||||
|
||||
// Sauvegarder la configuration locale cluster et réseau pour ne pas l'écraser par celle du master
|
||||
$localClusterSettings = $settings;
|
||||
$localSettingsRow = null;
|
||||
|
||||
try {
|
||||
$localSettingsRow = $this->db->query("SELECT local_net, local_networks_json, admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
// Restauration officielle en ligne via l'API SQLite (SQLite3::backup ou sqlite3 .restore)
|
||||
// Ne détruit aucun descripteur de fichier, ne touche pas aux journaux WAL/SHM,
|
||||
// et n'invalide aucune lecture concurrente dans PHP-FPM.
|
||||
if (file_exists($dbPath)) {
|
||||
$this->restoreSqliteDatabase($extractedDb, $dbPath);
|
||||
} else {
|
||||
@copy($extractedDb, $dbPath);
|
||||
@chmod($dbPath, 0660);
|
||||
@chown($dbPath, 'www-data');
|
||||
@chgrp($dbPath, 'asterisk');
|
||||
}
|
||||
|
||||
@unlink($extractedDb);
|
||||
|
||||
// Contrôle d'intégrité immédiat après synchronisation
|
||||
$postCheck = $this->checkSqliteIntegrity($dbPath);
|
||||
if ($postCheck !== 'ok') {
|
||||
if (file_exists($dbPath . '.pre_cluster_sync.bak')) {
|
||||
$this->restoreSqliteDatabase($dbPath . '.pre_cluster_sync.bak', $dbPath);
|
||||
$written = @file_put_contents($tmpArchive, $archiveRaw);
|
||||
if ($written === false || $written !== strlen($archiveRaw)) {
|
||||
throw new RuntimeException("Espace insuffisant dans /tmp lors de l'écriture de l'archive HA.");
|
||||
}
|
||||
|
||||
// Extraction sécurisée dans un dossier temporaire pour validation préalable
|
||||
@mkdir($tmpExtractDir, 0755, true);
|
||||
exec("tar -xzf " . escapeshellarg($tmpArchive) . " -C " . escapeshellarg($tmpExtractDir) . " 2>&1", $extOut, $extCode);
|
||||
|
||||
$extractedDb = $tmpExtractDir . '/database.sqlite';
|
||||
if (!file_exists($extractedDb) || filesize($extractedDb) === 0) {
|
||||
$detail = !empty($extOut) ? ' (' . implode(' | ', array_slice($extOut, 0, 2)) . ')' : '';
|
||||
throw new RuntimeException("L'archive de réplication reçue ne contient pas de base de données valide{$detail}.");
|
||||
}
|
||||
|
||||
// Contrôle d'intégrité SQLite strict du fichier reçu AVANT toute modification du système local
|
||||
$check = $this->checkSqliteIntegrity($extractedDb);
|
||||
if ($check !== 'ok') {
|
||||
throw new RuntimeException("La base de données reçue est corrompue ({$check}) : rejet strict de l'application.");
|
||||
}
|
||||
|
||||
// Sauvegarde d'urgence de la base actuelle avant synchronisation
|
||||
$dbPath = Database::getDatabasePath();
|
||||
if (file_exists($dbPath)) {
|
||||
@copy($dbPath, $dbPath . '.pre_cluster_sync.bak');
|
||||
}
|
||||
|
||||
// Sauvegarder la configuration locale cluster et réseau pour ne pas l'écraser par celle du master
|
||||
try {
|
||||
$localSettingsRow = $this->db->query("SELECT local_net, local_networks_json, admin_https_port FROM settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
// Restauration officielle en ligne via l'API SQLite (SQLite3::backup ou sqlite3 .restore)
|
||||
if (file_exists($dbPath)) {
|
||||
$this->restoreSqliteDatabase($extractedDb, $dbPath);
|
||||
} else {
|
||||
@copy($extractedDb, $dbPath);
|
||||
@chmod($dbPath, 0660);
|
||||
@chown($dbPath, 'www-data');
|
||||
@chgrp($dbPath, 'asterisk');
|
||||
}
|
||||
|
||||
@unlink($extractedDb);
|
||||
|
||||
// Contrôle d'intégrité immédiat après synchronisation
|
||||
$postCheck = $this->checkSqliteIntegrity($dbPath);
|
||||
if ($postCheck !== 'ok') {
|
||||
if (file_exists($dbPath . '.pre_cluster_sync.bak')) {
|
||||
$this->restoreSqliteDatabase($dbPath . '.pre_cluster_sync.bak', $dbPath);
|
||||
}
|
||||
throw new RuntimeException("Échec de vérification d'intégrité après synchronisation ({$postCheck}). Restauration préventive effectuée.");
|
||||
}
|
||||
|
||||
// Synchronisation miroir des sons personnalisés (avec suppression des sons effacés)
|
||||
$extractedCustom = $tmpExtractDir . '/custom';
|
||||
if (is_dir($extractedCustom) && is_dir('/var/lib/asterisk/sounds')) {
|
||||
shell_exec("rsync -a --delete " . escapeshellarg($extractedCustom . '/') . " /var/lib/asterisk/sounds/custom/ 2>/dev/null || true");
|
||||
shell_exec("chown -R asterisk:asterisk /var/lib/asterisk/sounds/custom 2>/dev/null || true");
|
||||
}
|
||||
|
||||
// Synchronisation miroir de la messagerie vocale (avec suppression des messages effacés)
|
||||
$extractedVm = $tmpExtractDir . '/voicemail';
|
||||
if (is_dir($extractedVm) && is_dir('/var/spool/asterisk/voicemail')) {
|
||||
shell_exec("rsync -a --delete " . escapeshellarg($extractedVm . '/') . " /var/spool/asterisk/voicemail/ 2>/dev/null || true");
|
||||
shell_exec("chown -R asterisk:asterisk /var/spool/asterisk/voicemail 2>/dev/null || true");
|
||||
}
|
||||
|
||||
// Synchronisation miroir des avatars de l'annuaire / UCP
|
||||
$extractedAvatars = $tmpExtractDir . '/avatars';
|
||||
if (is_dir($extractedAvatars)) {
|
||||
if (!is_dir('/var/www/nexus-pbx/public/avatars')) {
|
||||
@mkdir('/var/www/nexus-pbx/public/avatars', 0775, true);
|
||||
}
|
||||
shell_exec("rsync -a --delete " . escapeshellarg($extractedAvatars . '/') . " /var/www/nexus-pbx/public/avatars/ 2>/dev/null || true");
|
||||
shell_exec("chown -R www-data:asterisk /var/www/nexus-pbx/public/avatars 2>/dev/null || true");
|
||||
shell_exec("chmod -R 775 /var/www/nexus-pbx/public/avatars 2>/dev/null || true");
|
||||
}
|
||||
|
||||
// Restauration de l'astdb (contacts et IP actives des téléphones PJSIP) via le helper root
|
||||
$extractedAstdb = $tmpExtractDir . '/astdb_cluster_sync_' . $uniqRecv . '.sqlite3';
|
||||
if (!file_exists($extractedAstdb)) {
|
||||
$candidates = glob($tmpExtractDir . '/astdb_cluster_sync*.sqlite3') ?: [];
|
||||
if (!empty($candidates)) {
|
||||
$extractedAstdb = $candidates[0];
|
||||
}
|
||||
}
|
||||
if (file_exists($extractedAstdb)) {
|
||||
shell_exec('sudo -n ' . escapeshellcmd(self::CLUSTER_HELPER_PATH) . ' restore-astdb ' . escapeshellarg($extractedAstdb) . ' 2>&1');
|
||||
@unlink($extractedAstdb);
|
||||
}
|
||||
} finally {
|
||||
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
|
||||
@unlink($tmpArchive);
|
||||
throw new RuntimeException("Échec de vérification d'intégrité après synchronisation ({$postCheck}). Restauration préventive effectuée.");
|
||||
}
|
||||
|
||||
// Synchronisation miroir des sons personnalisés (avec suppression des sons effacés)
|
||||
$extractedCustom = $tmpExtractDir . '/custom';
|
||||
if (is_dir($extractedCustom) && is_dir('/var/lib/asterisk/sounds')) {
|
||||
shell_exec("rsync -a --delete " . escapeshellarg($extractedCustom . '/') . " /var/lib/asterisk/sounds/custom/ 2>/dev/null || true");
|
||||
shell_exec("chown -R asterisk:asterisk /var/lib/asterisk/sounds/custom 2>/dev/null || true");
|
||||
}
|
||||
|
||||
// Synchronisation miroir de la messagerie vocale (avec suppression des messages effacés)
|
||||
$extractedVm = $tmpExtractDir . '/voicemail';
|
||||
if (is_dir($extractedVm) && is_dir('/var/spool/asterisk/voicemail')) {
|
||||
shell_exec("rsync -a --delete " . escapeshellarg($extractedVm . '/') . " /var/spool/asterisk/voicemail/ 2>/dev/null || true");
|
||||
shell_exec("chown -R asterisk:asterisk /var/spool/asterisk/voicemail 2>/dev/null || true");
|
||||
}
|
||||
|
||||
// Synchronisation miroir des avatars de l'annuaire / UCP
|
||||
$extractedAvatars = $tmpExtractDir . '/avatars';
|
||||
if (is_dir($extractedAvatars)) {
|
||||
if (!is_dir('/var/www/nexus-pbx/public/avatars')) {
|
||||
@mkdir('/var/www/nexus-pbx/public/avatars', 0775, true);
|
||||
}
|
||||
shell_exec("rsync -a --delete " . escapeshellarg($extractedAvatars . '/') . " /var/www/nexus-pbx/public/avatars/ 2>/dev/null || true");
|
||||
shell_exec("chown -R www-data:asterisk /var/www/nexus-pbx/public/avatars 2>/dev/null || true");
|
||||
shell_exec("chmod -R 775 /var/www/nexus-pbx/public/avatars 2>/dev/null || true");
|
||||
}
|
||||
|
||||
// Restauration de l'astdb (contacts et IP actives des téléphones PJSIP) via le helper root
|
||||
$extractedAstdb = $tmpExtractDir . '/astdb_cluster_sync_' . $uniqRecv . '.sqlite3';
|
||||
if (!file_exists($extractedAstdb)) {
|
||||
$candidates = glob($tmpExtractDir . '/astdb_cluster_sync*.sqlite3') ?: [];
|
||||
if (!empty($candidates)) {
|
||||
$extractedAstdb = $candidates[0];
|
||||
}
|
||||
}
|
||||
if (file_exists($extractedAstdb)) {
|
||||
shell_exec('sudo -n ' . escapeshellcmd(self::CLUSTER_HELPER_PATH) . ' restore-astdb ' . escapeshellarg($extractedAstdb) . ' 2>&1');
|
||||
@unlink($extractedAstdb);
|
||||
}
|
||||
|
||||
shell_exec('rm -rf ' . escapeshellarg($tmpExtractDir) . ' 2>/dev/null || true');
|
||||
@unlink($tmpArchive);
|
||||
|
||||
// Réouverture de la connexion DB fraîche
|
||||
$newDb = Database::getConnection(true);
|
||||
$this->db = $newDb;
|
||||
@@ -1119,6 +1120,31 @@ class ClusterManager
|
||||
];
|
||||
}
|
||||
|
||||
private function cleanupStaleClusterTempFiles(): void
|
||||
{
|
||||
$cutoff = time() - 600;
|
||||
$patterns = [
|
||||
'/tmp/nexus_cluster_extract_*',
|
||||
'/tmp/nexus_cluster_tar_*',
|
||||
'/tmp/nexus_cluster_recv_*',
|
||||
'/tmp/nexus_cluster_sync_*',
|
||||
'/tmp/nexus_cluster_db_*',
|
||||
'/tmp/astdb_cluster_sync*',
|
||||
];
|
||||
foreach ($patterns as $pattern) {
|
||||
foreach (glob($pattern) ?: [] as $path) {
|
||||
$mtime = @filemtime($path);
|
||||
if ($mtime !== false && $mtime < $cutoff) {
|
||||
if (is_dir($path)) {
|
||||
shell_exec('rm -rf ' . escapeshellarg($path) . ' 2>/dev/null || true');
|
||||
} else {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkSqliteIntegrity(string $filePath): string
|
||||
{
|
||||
if (!file_exists($filePath) || filesize($filePath) === 0) {
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"version": "2.5.1",
|
||||
"version": "2.5.2",
|
||||
"codename": "Pulsar UCP",
|
||||
"release_date": "2026-11-11",
|
||||
"min_php_version": "8.2",
|
||||
"changelog": [
|
||||
"Correctif v2.5.2 : purge systématique try/finally + auto-nettoyage des dossiers temporaires HA /tmp (élimine la saturation tmpfs 100% qui bloquait la réplication)",
|
||||
"Correctif v2.5.1 : auto-déploiement automatique Nginx/Fail2ban UCP même après un git pull manuel",
|
||||
"Correctif v2.5.1 : vérification d'intégrité & restauration SQLite HA via PDO / SQLite3::backup sans dépendance CLI sqlite3 et anti-collision /tmp",
|
||||
"Correctif v2.5.1 : rafraîchissement instantané de la liste des utilisateurs UCP à la création/suppression sans rechargement manuel de la page",
|
||||
|
||||
Reference in new issue
Block a user