diff --git a/cron_archivage.php b/cron_archivage.php index 20e14b8..8a609d5 100644 --- a/cron_archivage.php +++ b/cron_archivage.php @@ -445,7 +445,7 @@ function Executer_cron_archivage() { $motif = 'creation_initiale'; } - $resultat_generation = Creer_generation($db, $id_bateau, $motif, $zone_usine); + $resultat_generation = Creer_generation($db, $id_bateau, $motif, $zone_usine, $version_cible); if (!$resultat_generation['ok']) { echo " ÉCHEC de création de génération : " . $resultat_generation['raison'] . " - rejetée.\n"; @@ -458,9 +458,16 @@ function Executer_cron_archivage() else { $nom_base_cible = $generation_active['nom_base']; - echo " Génération existante : {$nom_base_cible}.\n"; - // Bloc suivant : rejeu des segments incrémentaux si la version a changé, - // via Extraire_segments_incrementaux() + exécution (même mécanique que Creer_generation). + // Migration du schéma AVANT chargement si la version logicielle a évolué (mise à jour du + // bateau) - les données de l'archive ont été produites avec le nouveau schéma (synthèse §46) : + $migration = Migrer_generation($db, $generation_active, $version_cible, $fichier_sql, $contenu_fichier_sql); + if (!$migration['ok']) + { + echo " ÉCHEC : " . $migration['raison'] . " - rejetée.\n"; + Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, $migration['raison'], $mmsi_recu, $mmsi_attendu_connu); + continue; + } + echo " Génération existante : {$nom_base_cible} (" . $migration['message'] . ").\n"; } $chargement = Charger_contenu_archive($nom_base_cible, $dechiffrement['sql']); if (!$chargement['ok']) diff --git a/cron_quarantaine.php b/cron_quarantaine.php index df77167..e20c7a9 100644 --- a/cron_quarantaine.php +++ b/cron_quarantaine.php @@ -195,7 +195,7 @@ function Executer_cron_quarantaine() continue; } $motif = ($origine_bdd === 'factory' || $origine_bdd === 'restauration') ? $origine_bdd : 'creation_initiale'; - $resultat_generation = Creer_generation($db, $id_bateau, $motif, $zone_usine); + $resultat_generation = Creer_generation($db, $id_bateau, $motif, $zone_usine, $version_cible); if (!$resultat_generation['ok']) { echo " - {$nom} : échec de création de génération : " . $resultat_generation['raison'] . " - rejetée.\n"; @@ -209,6 +209,16 @@ function Executer_cron_quarantaine() else { $nom_base_cible = $generation_active['nom_base']; + // Même migration préalable que cron_archivage.php (synthèse §46) : + $migration = Migrer_generation($db, $generation_active, $version_cible, $fichier_sql, $contenu_fichier_sql); + if (!$migration['ok']) + { + echo " - {$nom} : " . $migration['raison'] . " - rejetée.\n"; + Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', $migration['raison'], $mmsi_recu, $mmsi_attendu_connu); + if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true); + rename($chemin, REPERTOIRE_ERREURS . "/" . $nom); + continue; + } } $chargement = Charger_contenu_archive($nom_base_cible, $dechiffrement['sql']); diff --git a/fonctions_archivage.php b/fonctions_archivage.php index 0919042..c3c9eaf 100644 --- a/fonctions_archivage.php +++ b/fonctions_archivage.php @@ -140,22 +140,24 @@ function Decrypter_archive($contenu_chiffre) // ---------------------------------------------------------------------------- // Résout la génération active d'un bateau (cf synthèse §35.4). Renvoie -// ['existe'=>bool, 'cle'=>int|null, 'numero'=>int|null, 'nom_base'=>string|null] +// ['existe'=>bool, 'cle'=>int|null, 'numero'=>int|null, 'nom_base'=>string|null, +// 'version'=>string|null] - 'version' = Version_soft, version logicielle à +// laquelle correspond le schéma de cette base (synthèse §46) // - 'existe'=>false signifie "aucune génération active" (tout premier bateau, // cf synthèse §35.5 point 4), PAS une erreur. // ---------------------------------------------------------------------------- function Resoudre_generation_active($db, $id_bateau) { - $requete = "SELECT Cle_primaire, Numero_generation, Nom_base FROM TB_Generations_bateaux WHERE ID_Bateau=? AND Date_fin IS NULL"; + $requete = "SELECT Cle_primaire, Numero_generation, Nom_base, Version_soft FROM TB_Generations_bateaux WHERE ID_Bateau=? AND Date_fin IS NULL"; $stmt = $db->prepare($requete); $stmt->bind_param('i', $id_bateau); $stmt->execute(); - $stmt->bind_result($cle, $numero, $nom_base); + $stmt->bind_result($cle, $numero, $nom_base, $version_soft); $trouve = $stmt->fetch(); $stmt->close(); - if (!$trouve) return ['existe' => false, 'cle' => null, 'numero' => null, 'nom_base' => null]; - return ['existe' => true, 'cle' => $cle, 'numero' => $numero, 'nom_base' => $nom_base]; + if (!$trouve) return ['existe' => false, 'cle' => null, 'numero' => null, 'nom_base' => null, 'version' => null]; + return ['existe' => true, 'cle' => $cle, 'numero' => $numero, 'nom_base' => $nom_base, 'version' => $version_soft]; } // ---------------------------------------------------------------------------- @@ -259,7 +261,7 @@ function Adapter_nom_base($contenu_sql, $nom_base_cible) // updater.php) - un bloc SQL avec DROP/CREATE DATABASE ne se prête pas bien // à mysqli::multi_query. // ---------------------------------------------------------------------------- -function Creer_generation($db_principale, $id_bateau, $motif, $contenu_sql) +function Creer_generation($db_principale, $id_bateau, $motif, $contenu_sql, $version_soft) { $generation_actuelle = Resoudre_generation_active($db_principale, $id_bateau); $prochain_numero = $generation_actuelle['existe'] ? $generation_actuelle['numero'] + 1 : 1; @@ -293,9 +295,11 @@ function Creer_generation($db_principale, $id_bateau, $motif, $contenu_sql) $stmt->close(); } - $requete = "INSERT INTO TB_Generations_bateaux (ID_Bateau, Numero_generation, Nom_base, Motif) VALUES (?, ?, ?, ?)"; + // Version_soft : version de l'archive qui a provoqué cette génération - le schéma de la nouvelle base + // est celui de la zone usine du fichier SQL de cette version (synthèse §46) : + $requete = "INSERT INTO TB_Generations_bateaux (ID_Bateau, Numero_generation, Nom_base, Motif, Version_soft) VALUES (?, ?, ?, ?, ?)"; $stmt = $db_principale->prepare($requete); - $stmt->bind_param('iiss', $id_bateau, $prochain_numero, $nom_base, $motif); + $stmt->bind_param('iisss', $id_bateau, $prochain_numero, $nom_base, $motif, $version_soft); if (!$stmt->execute()) { $erreur = $db_principale->error; @@ -346,6 +350,125 @@ function Extraire_segments_incrementaux($contenu_fichier, $depart, $cible) return substr($contenu_fichier, $debut, $fin - $debut); } +// ---------------------------------------------------------------------------- +// Migre le schéma de la base d'une génération EXISTANTE vers la version +// logicielle d'une archive 'normale' plus récente (synthèse §32.2, §46) - cas +// d'une mise à jour logicielle du bateau (updater.php, Type="update"), qui +// laisse Origine_bdd à 'normale' (§35.6). À appeler AVANT le chargement de +// l'archive, dont les données ont été produites avec le nouveau schéma. +// +// Déroulé : +// - Version_soft de la génération = version de l'archive -> rien à faire. +// - Sinon : fichier SQL actuel (Version_soft -> TB_Versions -> TB_Versions_sql) +// et fichier SQL cible (déjà résolu par l'appelant) ; extraction, dans le +// fichier cible, de tout ce qui suit le marqueur "# Fin de " +// jusqu'au marqueur "# Fin de " inclus ; exécution sur la base de +// la génération (nom de base substitué dans les USE) ; puis mise à jour de +// Version_soft - même si aucune ligne n'était à exécuter (plusieurs +// versions logicielles peuvent partager le même fichier SQL). +// +// Version_soft n'est mise à jour qu'APRÈS une migration réussie, et AVANT le +// chargement : si le chargement échoue ensuite, la base est bien migrée et +// la version enregistrée le dit (pas de rejeu à tort). Si la migration +// échoue, Version_soft ne bouge pas. +// +// Limite connue : les ALTER TABLE ne sont pas annulables. Un segment qui +// échoue en cours de route laisse une base partiellement migrée -> rejet de +// l'archive avec le détail de l'erreur, intervention manuelle nécessaire. +// +// Renvoie ['ok'=>true, 'message'=>string] ou ['ok'=>false, 'raison'=>string]. +// ---------------------------------------------------------------------------- +function Migrer_generation($db_principale, $generation_active, $version_cible, $fichier_cible, $contenu_fichier_cible) +{ + $version_actuelle = $generation_active['version']; + $nom_base = $generation_active['nom_base']; + + if ($version_actuelle === null || $version_actuelle === '') + { + return ['ok' => false, 'raison' => "version de schéma de la génération {$nom_base} inconnue (TB_Generations_bateaux.Version_soft vide) - à renseigner manuellement"]; + } + if ($version_actuelle === $version_cible) + { + return ['ok' => true, 'message' => "schéma déjà en version {$version_cible}"]; + } + + $fichier_actuel = Resoudre_fichier_sql($db_principale, $version_actuelle); + if ($fichier_actuel === false) + { + return ['ok' => false, 'raison' => "version actuelle '{$version_actuelle}' de {$nom_base} inconnue de TB_Versions"]; + } + + $segments = Extraire_segments_incrementaux($contenu_fichier_cible, $fichier_actuel, $fichier_cible); + if ($segments === false) + { + return ['ok' => false, 'raison' => "migration {$version_actuelle} ({$fichier_actuel}) -> {$version_cible} ({$fichier_cible}) impossible : marqueur introuvable dans {$fichier_cible}, ou version cible antérieure à la version actuelle"]; + } + + $nb_requetes = 0; + if ($segments !== '') + { + // Les segments commencent par "USE `shipheart`;" (base du CM5) : substitution ciblée vers la base de + // la génération, comme pour la zone usine (Adapter_nom_base). Puis contrôle qu'il ne reste AUCUN + // USE vers une autre base - une migration ne doit jamais pouvoir s'exécuter ailleurs que sur la base + // de ce bateau : + [$sql_adapte, ] = Adapter_nom_base($segments, $nom_base); + if (preg_match_all('/^\s*USE\s+`?([A-Za-z0-9_]+)`?\s*;/mi', $sql_adapte, $m)) + { + foreach ($m[1] as $base_visee) + { + if ($base_visee !== $nom_base) + { + return ['ok' => false, 'raison' => "segment de migration visant une autre base ('{$base_visee}') - exécution refusée"]; + } + } + } + + $execution = Executer_sql_base($nom_base, $sql_adapte); + if (!$execution['ok']) + { + return ['ok' => false, 'raison' => "échec de la migration {$version_actuelle} -> {$version_cible} sur {$nom_base} (base possiblement partiellement migrée) : " . $execution['raison']]; + } + // Nombre de requêtes exécutées, à titre indicatif dans le journal (hors USE) : + $nb_requetes = preg_match_all('/;\s*$/m', $sql_adapte) - preg_match_all('/^\s*USE\s+[^;]*;\s*$/mi', $sql_adapte); + } + + $requete = "UPDATE TB_Generations_bateaux SET Version_soft=? WHERE Cle_primaire=?"; + $stmt = $db_principale->prepare($requete); + $stmt->bind_param('si', $version_cible, $generation_active['cle']); + $ok = $stmt->execute(); + $stmt->close(); + if (!$ok) + { + return ['ok' => false, 'raison' => "migration {$version_actuelle} -> {$version_cible} appliquée sur {$nom_base}, mais Version_soft n'a pas pu être mise à jour : " . $db_principale->error . " - à corriger manuellement avant la prochaine archive"]; + } + + if ($nb_requetes === 0) + { + return ['ok' => true, 'message' => "version {$version_actuelle} -> {$version_cible}, même fichier SQL ou aucun changement de schéma"]; + } + return ['ok' => true, 'message' => "schéma migré {$version_actuelle} -> {$version_cible} ({$nb_requetes} requête(s))"]; +} + +// ---------------------------------------------------------------------------- +// Exécute un bloc SQL sur une base bateau via le client mysql en ligne de +// commande (même mécanique que Creer_generation/Charger_contenu_archive - un +// bloc contenant des ALTER/CREATE/DROP se prête mal à mysqli::multi_query). +// Renvoie ['ok'=>true] ou ['ok'=>false, 'raison'=>string]. +// ---------------------------------------------------------------------------- +function Executer_sql_base($nom_base, $sql) +{ + $fichier_temp = tempnam(sys_get_temp_dir(), 'migr_'); + file_put_contents($fichier_temp, $sql); + $commande = "mysql --host=" . escapeshellarg(BDD_BATEAUX_HOST) . " --port=" . escapeshellarg(BDD_BATEAUX_PORT) + . " --user=" . escapeshellarg(BDD_BATEAUX_USER) . " --password=" . escapeshellarg(BDD_BATEAUX_PASSWORD) + . " " . escapeshellarg($nom_base) . " < " . escapeshellarg($fichier_temp) . " 2>&1"; + exec($commande, $sortie, $code_retour); + unlink($fichier_temp); + + if ($code_retour !== 0) return ['ok' => false, 'raison' => implode(" | ", $sortie)]; + return ['ok' => true]; +} + // ---------------------------------------------------------------------------- // Détermine la liste des tables fragmentées (incrémentales) d'une base bateau, // par la MÊME méthode que côté CM5 (SHOW TABLES LIKE 'TB_Mesures%' + TB_Historique, diff --git a/sql/version_soft_generations.sql b/sql/version_soft_generations.sql new file mode 100644 index 0000000..5d3291b --- /dev/null +++ b/sql/version_soft_generations.sql @@ -0,0 +1,14 @@ +-- ============================================================================ +-- Ajout de TB_Generations_bateaux.Version_soft (synthèse §46) +-- Version logicielle (TB_Versions.Nom, ex : "4.1.6") à laquelle correspond le +-- schéma de la base de la génération. Renseignée à la création de la +-- génération (version de l'archive qui l'a provoquée), puis à chaque +-- migration incrémentale réussie (archive 'normale' de version plus récente). +-- NULL = inconnue (générations créées avant l'ajout de la colonne) : une +-- génération ACTIVE à NULL fait rejeter les archives du bateau, jusqu'à +-- saisie manuelle de la bonne valeur. +-- ============================================================================ +ALTER TABLE TB_Generations_bateaux + ADD COLUMN Version_soft VARCHAR(20) NULL + COMMENT 'Version logicielle (TB_Versions.Nom) du schéma de cette base - NULL si inconnue' + AFTER Motif;