Chantier archivage flotte : restructuration par boîtier (pas seulement les bateaux affectés), quarantaine MMSI/sans_affectation avec script de réexamen dédié (cron_quarantaine.php), rejet des erreurs techniques non transitoires, fonctions communes extraites (fonctions_archivage.php), renommages de cohérence (TB_Affectations_bateaux, TB_Generations_bateaux, ID_Bateau_affecte), traçabilité MMSI complète (cf synthèse §38-39)

This commit is contained in:
2026-10-02 09:21:59 +00:00
parent 1dd4d0c288
commit 3a9056f5bc
5 changed files with 1084 additions and 0 deletions
+480
View File
@@ -0,0 +1,480 @@
<?php
// ============================================================================
// cron_archivage.php - Traitement périodique des archives de la flotte
// (cf synthèse §32-36). Exécuté via cron, dans le conteneur ship2net-php :
// docker exec ship2net-php php /var/www/cron_archivage.php
//
// Construction progressive, bloc par bloc :
// Bloc 1 (ce fichier) : résolution des bateaux actifs + liste des archives
// en attente sur chacun.
// Blocs suivants (à venir) : récupération, vérification d'intégrité,
// résolution de génération, chargement, confirmation.
// ============================================================================
// Certains fichiers du site (fonctions_mail.php notamment) utilisent des
// chemins relatifs ("../vendor/autoload.php"), qui se résolvent par rapport
// au répertoire de travail du processus - pas par rapport à l'emplacement du
// fichier qui les appelle. Ça fonctionnait par accident tant qu'Apache fixait
// ce répertoire à /var/www/html (la racine web). Fixé explicitement ici pour
// ne jamais dépendre de l'endroit d'où ce script est lancé - crucial pour une
// tâche cron, qui n'a généralement aucun répertoire de travail prévisible.
chdir("/var/www/html");
require_once __DIR__ . "/config/config.php";
require_once __DIR__ . "/config/fonctions_bdd.php";
require_once __DIR__ . "/fonctions_archivage.php";
// ----------------------------------------------------------------------------
// Port d'écoute de shipheart_api sur chaque CM5 (cf synthèse §33.1).
// ----------------------------------------------------------------------------
const PORT_API_CM5 = 8080;
// Délais réseau volontairement courts : un bateau injoignable ne doit jamais
// bloquer longtemps le traitement des autres (cf synthèse §33.1 - le cron
// retentera au prochain cycle, ce n'est jamais une perte).
const TIMEOUT_CONNEXION_CM5 = 5;
const TIMEOUT_REQUETE_CM5 = 15;
// ----------------------------------------------------------------------------
// Liste TOUS les boîtiers connus de ship2net (TB_Cartes avec une adresse
// NetBird renseignée) - pas seulement ceux affectés à un bateau. Nécessaire
// pour pouvoir mettre en quarantaine les archives d'un boîtier jamais affecté
// ou retiré, pas seulement celles d'une divergence MMSI (cf synthèse §38).
// Un boîtier neuf doit donc être déclaré dans TB_Cartes (adresse NetBird
// renseignée) avant toute affectation, pour entrer dans cette liste.
// Renvoie un tableau de ['Adresse_mac' => string, 'Adresse_netbird' => string].
// ----------------------------------------------------------------------------
function Lister_boitiers_connus($db)
{
$requete = "SELECT Adresse_mac, Adresse_netbird FROM TB_Cartes
WHERE Adresse_netbird IS NOT NULL AND Adresse_netbird <> ''";
$resultat = $db->query($requete);
$boitiers = [];
while ($ligne = $resultat->fetch_assoc()) $boitiers[] = $ligne;
return $boitiers;
}
// ----------------------------------------------------------------------------
// Interroge GET /archives sur le boîtier d'un bateau. Renvoie :
// ['ok' => true, 'archives' => [...]] en cas de succès
// ['ok' => false, 'raison' => "..."] sinon (bateau injoignable, réponse
// invalide...) - jamais une erreur
// fatale, le cron doit continuer
// avec les autres bateaux.
// Le tri chronologique est déjà garanti côté API par le nom de fichier
// (cf synthèse §32.4) ; retrié ici aussi par précaution, sans coût réel.
// ----------------------------------------------------------------------------
function Lister_archives_en_attente($adresse_netbird)
{
if (empty($adresse_netbird))
{
return ['ok' => false, 'raison' => "aucune adresse NetBird enregistrée pour ce boîtier"];
}
$url = "http://" . $adresse_netbird . ":" . PORT_API_CM5 . "/archives";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, TIMEOUT_CONNEXION_CM5);
curl_setopt($ch, CURLOPT_TIMEOUT, TIMEOUT_REQUETE_CM5);
$reponse_brute = curl_exec($ch);
$erreur_curl = curl_error($ch);
$code_http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($reponse_brute === false)
{
return ['ok' => false, 'raison' => "injoignable (" . $erreur_curl . ")"];
}
if ($code_http !== 200)
{
return ['ok' => false, 'raison' => "réponse HTTP " . $code_http];
}
$donnees = json_decode($reponse_brute, true);
if (!is_array($donnees) || ($donnees['status'] ?? '') !== 'ok' || !isset($donnees['archives']))
{
return ['ok' => false, 'raison' => "réponse JSON invalide ou inattendue"];
}
$archives = $donnees['archives'];
usort($archives, fn($a, $b) => strcmp($a['nom'], $b['nom']));
return ['ok' => true, 'archives' => $archives];
}
// ----------------------------------------------------------------------------
// Extrait l'empreinte (16 hex) portée par le nom de fichier - même format,
// exactement, que la validation déjà faite côté shipheart_api (§33.1).
// Renvoie false si le nom ne correspond pas au format attendu.
// ----------------------------------------------------------------------------
function Extraire_empreinte_nom($nom_fichier)
{
if (!preg_match('/^[0-9A-Fa-f]{12}_\d{6}_\d{6}_([0-9A-Fa-f]{16})\.sql\.gz\.enc$/', $nom_fichier, $m)) return false;
return strtolower($m[1]);
}
// ----------------------------------------------------------------------------
// GET /archives/<nom> - récupère le contenu brut d'une archive précise.
// Même forme de retour que Lister_archives_en_attente() (['ok' => bool, ...]).
// ----------------------------------------------------------------------------
function Recuperer_archive($adresse_netbird, $nom_fichier)
{
$url = "http://" . $adresse_netbird . ":" . PORT_API_CM5 . "/archives/" . rawurlencode($nom_fichier);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, TIMEOUT_CONNEXION_CM5);
curl_setopt($ch, CURLOPT_TIMEOUT, TIMEOUT_REQUETE_CM5);
$contenu = curl_exec($ch);
$erreur_curl = curl_error($ch);
$code_http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($contenu === false) return ['ok' => false, 'raison' => "injoignable (" . $erreur_curl . ")"];
if ($code_http !== 200) return ['ok' => false, 'raison' => "réponse HTTP " . $code_http];
return ['ok' => true, 'contenu' => $contenu];
}
// ----------------------------------------------------------------------------
// Vérifie que le contenu récupéré correspond bien à l'empreinte portée par
// son nom - détecte une corruption de transfert (cf synthèse §32.4/33.1).
// Ne jamais supprimer l'archive côté CM5 si cette vérification échoue - une
// nouvelle tentative au prochain cycle récupérera à nouveau le même fichier.
// ----------------------------------------------------------------------------
function Integrite_confirmee($nom_fichier, $contenu)
{
$empreinte_attendue = Extraire_empreinte_nom($nom_fichier);
if ($empreinte_attendue === false) return false;
$empreinte_calculee = substr(hash('sha256', $contenu), 0, 16);
return $empreinte_calculee === $empreinte_attendue;
}
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Copie de sauvegarde des archives mises en quarantaine (MMSI ou affectation absente) - avant de
// confirmer la suppression côté CM5, pour ne jamais perdre le contenu pendant que la quarantaine est en
// attente (cf synthèse §38, correction d'un manque identifié en testant le mécanisme).
// ----------------------------------------------------------------------------
// Met une archive en quarantaine (divergence MMSI) : sauvegarde son contenu
// CHIFFRÉ tel que reçu (pour ne rien perdre pendant que la quarantaine est en
// attente - correction d'un manque identifié en testant le mécanisme, cf
// synthèse §38), enregistre la ligne dans TB_Archives_traitees, puis confirme
// la suppression côté CM5 (son intégrité a déjà été vérifiée avant cet appel).
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Met une archive en quarantaine faute d'affectation active (boîtier jamais
// affecté, ou retiré - cf synthèse §38). Contrairement à la quarantaine MMSI,
// aucun déchiffrement n'est nécessaire : sans bateau à qui l'attribuer, ni
// l'en-tête ni le contenu ne servent à rien à ce stade.
// ----------------------------------------------------------------------------
function Mettre_en_quarantaine_sans_affectation($db_principale, $adresse_netbird, $nom_fichier, $contenu_chiffre, $adresse_mac)
{
if (!is_dir(REPERTOIRE_QUARANTAINE)) @mkdir(REPERTOIRE_QUARANTAINE, 0750, true);
file_put_contents(REPERTOIRE_QUARANTAINE . "/" . $nom_fichier, $contenu_chiffre);
$statut = 'en_quarantaine_sans_affectation';
$requete = "INSERT INTO TB_Archives_traitees (Nom_fichier, Adresse_mac, Statut) VALUES (?, ?, ?)";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('sss', $nom_fichier, $adresse_mac, $statut);
$stmt->execute();
$stmt->close();
Confirmer_suppression_archive($adresse_netbird, $nom_fichier);
}
// ----------------------------------------------------------------------------
// Rejette une archive suite à une erreur technique NON transitoire (cf
// synthèse §38) : déchiffrement, version inconnue, fichier SQL introuvable,
// échec de création de génération, échec de chargement. Sauvegarde le
// contenu chiffré dans un répertoire dédié (suivi manuel, reprise éventuelle
// une fois la cause corrigée), enregistre Statut='rejetee' avec le détail de
// l'erreur, puis confirme la suppression côté CM5 - inutile de laisser le
// CM5 retransmettre indéfiniment un fichier dont le contenu ne changera
// jamais.
// ----------------------------------------------------------------------------
function Rejeter_archive($db_principale, $adresse_netbird, $nom_fichier, $contenu_chiffre, $adresse_mac, $id_bateau, $detail_erreur, $mmsi_recu = null, $mmsi_attendu = null)
{
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
file_put_contents(REPERTOIRE_ERREURS . "/" . $nom_fichier, $contenu_chiffre);
$statut = 'rejetee';
$requete = "INSERT INTO TB_Archives_traitees
(Nom_fichier, Adresse_mac, ID_Bateau_affecte, ID_Bateau_final, Statut, Detail_erreur, MMSI_recu, MMSI_attendu, Date_resolution)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('ssiissss', $nom_fichier, $adresse_mac, $id_bateau, $id_bateau, $statut, $detail_erreur, $mmsi_recu, $mmsi_attendu);
$stmt->execute();
$stmt->close();
Confirmer_suppression_archive($adresse_netbird, $nom_fichier);
}
function Mettre_en_quarantaine_mmsi($db_principale, $adresse_netbird, $nom_fichier, $contenu_chiffre, $adresse_mac, $id_bateau, $mmsi_recu, $mmsi_attendu)
{
if (!is_dir(REPERTOIRE_QUARANTAINE)) @mkdir(REPERTOIRE_QUARANTAINE, 0750, true);
file_put_contents(REPERTOIRE_QUARANTAINE . "/" . $nom_fichier, $contenu_chiffre);
$statut = 'en_quarantaine_mmsi';
$requete = "INSERT INTO TB_Archives_traitees
(Nom_fichier, Adresse_mac, ID_Bateau_affecte, Statut, MMSI_recu, MMSI_attendu)
VALUES (?, ?, ?, ?, ?, ?)";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('ssisss', $nom_fichier, $adresse_mac, $id_bateau, $statut, $mmsi_recu, $mmsi_attendu);
$stmt->execute();
$stmt->close();
Confirmer_suppression_archive($adresse_netbird, $nom_fichier);
}
// ----------------------------------------------------------------------------
// Vérifie si cette archive a déjà été traitée (quel que soit le résultat) -
// garde-fou anti-double-traitement, en complément de l'index UNIQUE déjà
// présent sur TB_Archives_traitees.Nom_fichier (cf synthèse §32.6/34.3).
// ----------------------------------------------------------------------------
function Archive_deja_traitee($db_principale, $nom_fichier)
{
$requete = "SELECT 1 FROM TB_Archives_traitees WHERE Nom_fichier=?";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('s', $nom_fichier);
$stmt->execute();
$stmt->store_result();
$trouve = $stmt->num_rows > 0;
$stmt->close();
return $trouve;
}
// ----------------------------------------------------------------------------
// Enregistre le résultat du traitement d'une archive dans TB_Archives_traitees
// (cf synthèse §34.3). $id_generation peut être null (non encore résolu, ex :
// bateau sans aucune affectation).
// ----------------------------------------------------------------------------
function Enregistrer_archive_traitee($db_principale, $nom_fichier, $adresse_mac, $id_bateau, $id_generation, $statut, $mmsi_recu = null, $mmsi_attendu = null)
{
// MMSI_recu/MMSI_attendu toujours renseignés quand connus, même en cas de concordance parfaite -
// trace d'historique, et par principe : un NULL ne doit jamais pouvoir signifier à la fois "MMSI
// inconnu" et "MMSI connu mais non indiqué ici" (cf synthèse §39).
$requete = "INSERT INTO TB_Archives_traitees
(Nom_fichier, Adresse_mac, ID_Bateau_affecte, ID_Bateau_final, ID_Generation, Statut, MMSI_recu, MMSI_attendu, Date_resolution)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('ssiiisss', $nom_fichier, $adresse_mac, $id_bateau, $id_bateau, $id_generation, $statut, $mmsi_recu, $mmsi_attendu);
$ok = $stmt->execute();
$stmt->close();
return $ok;
}
// ----------------------------------------------------------------------------
// DELETE /archives/<nom> - confirme le traitement auprès du CM5, qui peut
// alors supprimer le fichier local (cf synthèse §33.1).
// ----------------------------------------------------------------------------
function Confirmer_suppression_archive($adresse_netbird, $nom_fichier)
{
$url = "http://" . $adresse_netbird . ":" . PORT_API_CM5 . "/archives/" . rawurlencode($nom_fichier);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, TIMEOUT_CONNEXION_CM5);
curl_setopt($ch, CURLOPT_TIMEOUT, TIMEOUT_REQUETE_CM5);
$reponse = curl_exec($ch);
$code_http = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return ($reponse !== false && $code_http === 200);
}
// ----------------------------------------------------------------------------
// Bloc principal - pour l'instant, se limite à afficher ce qui serait traité.
// Les blocs suivants remplaceront cet affichage par le traitement réel.
// ----------------------------------------------------------------------------
function Executer_cron_archivage()
{
// Ce script tourne nativement sur le VPS (pas dans le conteneur ship2net-php) - contrairement au site
// web, il ne peut pas résoudre "ship2net-db" (nom DNS interne à Docker). Le port de ship2net-db a été
// publié sur 127.0.0.1:3307 uniquement (jamais exposé publiquement, ni même via NetBird) - le 3306
// natif du port est déjà pris par l'instance MariaDB native du VPS, un usage différent (cf synthèse §37).
Ouverture_bdd("127.0.0.1:3307", BDD_USER, BDD_PASSWORD, BDD_DATABASE);
$db = $_SESSION['db'];
$boitiers = Lister_boitiers_connus($db);
echo count($boitiers) . " boîtier(s) connu(s) avec une adresse NetBird renseignée.\n\n";
foreach ($boitiers as $boitier)
{
$adresse_mac = $boitier['Adresse_mac'];
$resultat = Lister_archives_en_attente($boitier['Adresse_netbird']);
if (!$resultat['ok'])
{
echo "Boîtier {$adresse_mac} : " . $resultat['raison'] . " - ignoré pour ce cycle.\n";
continue;
}
$nb = count($resultat['archives']);
if ($nb === 0) continue; // rien à signaler pour ce boîtier ce cycle-ci
// Résolu UNE SEULE FOIS par boîtier (pas par archive) : toutes les archives d'un même passage
// partagent la même adresse MAC, et l'affectation ne change jamais en cours d'exécution du
// script - seul un geste administratif explicite la modifie (cf synthèse §38).
$affectation = Resoudre_affectation_active($db, $adresse_mac);
$libelle = $affectation['existe'] ? "Bateau {$affectation['id_bateau']}" : "Boîtier {$adresse_mac} (aucune affectation active)";
echo "{$libelle} : {$nb} archive(s) en attente.\n";
foreach ($resultat['archives'] as $archive)
{
$nom = $archive['nom'];
if (Archive_deja_traitee($db, $nom))
{
echo " - {$nom} : déjà traitée - confirmation de suppression seulement.\n";
Confirmer_suppression_archive($boitier['Adresse_netbird'], $nom);
continue;
}
$recuperation = Recuperer_archive($boitier['Adresse_netbird'], $nom);
if (!$recuperation['ok'])
{
echo " - {$nom} : ÉCHEC de récupération (" . $recuperation['raison'] . ") - nouvelle tentative au prochain cycle.\n";
continue;
}
if (!Integrite_confirmee($nom, $recuperation['contenu']))
{
echo " - {$nom} : empreinte invalide, archive potentiellement corrompue - nouvelle tentative au prochain cycle.\n";
continue;
}
// Aucune affectation active (jamais affecté, ou retiré) : quarantaine directe, sans
// déchiffrement - ni l'en-tête ni le contenu ne servent à rien sans bateau à qui l'attribuer
// (cf synthèse §38) :
if (!$affectation['existe'])
{
echo " - {$nom} : aucune affectation active pour ce boîtier - mise en quarantaine.\n";
Mettre_en_quarantaine_sans_affectation($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac);
continue;
}
$id_bateau = $affectation['id_bateau'];
$dechiffrement = Decrypter_archive($recuperation['contenu']);
if (!$dechiffrement['ok'])
{
echo " - {$nom} : ÉCHEC de déchiffrement (" . $dechiffrement['raison'] . ") - rejetée.\n";
Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, "Échec de déchiffrement : " . $dechiffrement['raison']);
continue;
}
$origine_bdd = $dechiffrement['entete']['Origine_bdd'] ?? 'normale';
$mmsi_recu = $dechiffrement['entete']['No_mmsi'] ?? null;
$mmsi_attendu_connu = null; // reste null si No_mmsi absent de l'en-tête - sinon toujours renseigné (cf synthèse §39)
if ($mmsi_recu !== null)
{
$verification_mmsi = Verifier_mmsi($db, $id_bateau, $mmsi_recu);
$mmsi_attendu_connu = $verification_mmsi['attendu'];
if ($verification_mmsi['statut'] === 'quarantaine')
{
echo " - {$nom} : MMSI reçu ({$mmsi_recu}) différent de celui attendu ({$verification_mmsi['attendu']}) - mise en quarantaine.\n";
Mettre_en_quarantaine_mmsi($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, $mmsi_recu, $verification_mmsi['attendu']);
continue;
}
}
$version_cible = $dechiffrement['entete']['Version_soft_no_package'] ?? null;
$generation_active = Resoudre_generation_active($db, $id_bateau);
$nouvelle_generation = Nouvelle_generation_necessaire($origine_bdd, $generation_active);
echo " - {$nom} : récupérée et intègre (" . strlen($recuperation['contenu']) . " octets), origine=" . $origine_bdd . "\n";
$fichier_sql = Resoudre_fichier_sql($db, $version_cible);
if ($fichier_sql === false)
{
echo " ÉCHEC : version logicielle '{$version_cible}' inconnue de TB_Versions - rejetée.\n";
Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, "Version logicielle '{$version_cible}' inconnue de TB_Versions", $mmsi_recu, $mmsi_attendu_connu);
continue;
}
$contenu_fichier_sql = @file_get_contents(REPERTOIRE_VERSIONS_SQL . "/" . $fichier_sql);
if ($contenu_fichier_sql === false)
{
echo " ÉCHEC : fichier {$fichier_sql} introuvable sur le VPS - rejetée.\n";
Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, "Fichier {$fichier_sql} introuvable sur le VPS", $mmsi_recu, $mmsi_attendu_connu);
continue;
}
if ($nouvelle_generation)
{
$zone_usine = Extraire_zone_usine($contenu_fichier_sql);
if ($zone_usine === false)
{
echo " ÉCHEC : marqueur Factory_structure introuvable dans {$fichier_sql} - rejetée.\n";
Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, "Marqueur Factory_structure introuvable dans {$fichier_sql}", $mmsi_recu, $mmsi_attendu_connu);
continue;
}
// 'factory'/'restauration' est toujours un fait rapporté honnêtement par le bateau - à
// conserver tel quel pour le diagnostic, qu'une génération existait déjà côté serveur ou
// non. Seul 'normale' sans aucune génération existante (un bateau vraiment neuf, qui n'a
// jamais rien envoyé) devient 'creation_initiale' (cf synthèse §37).
if ($origine_bdd === 'factory' || $origine_bdd === 'restauration')
{
$motif = $origine_bdd;
}
else
{
$motif = 'creation_initiale';
}
$resultat_generation = Creer_generation($db, $id_bateau, $motif, $zone_usine);
if (!$resultat_generation['ok'])
{
echo " ÉCHEC de création de génération : " . $resultat_generation['raison'] . " - rejetée.\n";
Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, "Échec de création de génération : " . $resultat_generation['raison'], $mmsi_recu, $mmsi_attendu_connu);
continue;
}
$nom_base_cible = $resultat_generation['nom_base'];
echo " Nouvelle génération créée : {$nom_base_cible} (motif={$motif}).\n";
}
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).
}
$chargement = Charger_contenu_archive($nom_base_cible, $dechiffrement['sql']);
if (!$chargement['ok'])
{
echo " ÉCHEC de chargement dans {$nom_base_cible} : " . $chargement['raison'] . " - rejetée.\n";
Rejeter_archive($db, $boitier['Adresse_netbird'], $nom, $recuperation['contenu'], $adresse_mac, $id_bateau, "Échec de chargement dans {$nom_base_cible} : " . $chargement['raison'], $mmsi_recu, $mmsi_attendu_connu);
continue;
}
echo " Contenu chargé dans {$nom_base_cible}.\n";
// Résolution de la vraie Cle_primaire de la génération (celle juste créée, ou l'existante) :
$generation_pour_enregistrement = Resoudre_generation_active($db, $id_bateau);
Enregistrer_archive_traitee($db, $nom, $adresse_mac, $id_bateau, $generation_pour_enregistrement['cle'], 'traitee', $mmsi_recu, $mmsi_attendu_connu);
if (Confirmer_suppression_archive($boitier['Adresse_netbird'], $nom))
{
echo " Archive confirmée et supprimée côté CM5.\n";
}
else
{
echo " AVERTISSEMENT : confirmation de suppression échouée (le fichier restera côté CM5, sans conséquence - cf synthèse §32.5).\n";
}
}
}
Fermeture_bdd();
}
Executer_cron_archivage();
+220
View File
@@ -0,0 +1,220 @@
<?php
// ============================================================================
// cron_quarantaine.php - Réexamen périodique des archives en quarantaine
// (MMSI divergent, ou aucune affectation active - cf synthèse §38-39).
// Exécuté manuellement ou via cron, après qu'un administrateur ait mis à jour
// les données concernées (TB_Bateaux.MMSI, TB_Affectations_bateaux) :
// php /var/www/cron_quarantaine.php
//
// Ne retente PAS automatiquement : parcourt les fichiers du répertoire de
// quarantaine, et pour chacun, relit sa ligne TB_Archives_traitees existante
// (jamais une nouvelle ligne - une UPDATE, contrairement à
// cron_archivage.php) puis retente la résolution (affectation, puis MMSI).
// Si la cause est levée : traitement complet, identique à cron_archivage.php
// à partir de ce point. Sinon : le fichier reste en quarantaine, rien n'est
// modifié, nouvelle tentative au prochain lancement.
// ============================================================================
require_once __DIR__ . "/config/config.php";
require_once __DIR__ . "/config/fonctions_bdd.php";
require_once __DIR__ . "/fonctions_archivage.php";
// ----------------------------------------------------------------------------
// Relit une ligne TB_Archives_traitees encore en attente (Date_resolution
// IS NULL - donc nécessairement un des deux statuts de quarantaine) pour ce
// nom de fichier. Renvoie null si introuvable (incohérence à examiner
// manuellement : un fichier en quarantaine devrait toujours avoir sa ligne).
// ----------------------------------------------------------------------------
function Lire_ligne_en_attente($db, $nom_fichier)
{
$requete = "SELECT Statut FROM TB_Archives_traitees WHERE Nom_fichier=? AND Date_resolution IS NULL";
$stmt = $db->prepare($requete);
$stmt->bind_param('s', $nom_fichier);
$stmt->execute();
$stmt->bind_result($statut);
$trouve = $stmt->fetch();
$stmt->close();
if (!$trouve) return null;
return ['statut' => $statut];
}
// ----------------------------------------------------------------------------
// Met à jour une ligne TB_Archives_traitees existante (contrairement à
// cron_archivage.php, qui en crée toujours une nouvelle) - la ligne de
// quarantaine, créée au moment de la mise en attente, est ici tranchée
// définitivement (cf synthèse §39).
// ----------------------------------------------------------------------------
function Trancher_archive_en_attente($db, $nom_fichier, $id_bateau_final, $id_generation, $statut, $detail_erreur = null, $mmsi_recu = null, $mmsi_attendu = null)
{
// MMSI_recu/MMSI_attendu ne sont mis à jour QUE si connus à cet appel (une archive mise en quarantaine
// 'sans_affectation' n'avait pas encore de MMSI à comparer) - sinon, on conserve les valeurs déjà
// enregistrées au moment de la mise en quarantaine initiale, plutôt que de les écraser par NULL
// (cf synthèse §39) :
if ($mmsi_recu !== null)
{
$requete = "UPDATE TB_Archives_traitees
SET ID_Bateau_final=?, ID_Generation=?, Statut=?, Detail_erreur=?, MMSI_recu=?, MMSI_attendu=?, Date_resolution=NOW()
WHERE Nom_fichier=?";
$stmt = $db->prepare($requete);
$stmt->bind_param('iisssss', $id_bateau_final, $id_generation, $statut, $detail_erreur, $mmsi_recu, $mmsi_attendu, $nom_fichier);
}
else
{
$requete = "UPDATE TB_Archives_traitees
SET ID_Bateau_final=?, ID_Generation=?, Statut=?, Detail_erreur=?, Date_resolution=NOW()
WHERE Nom_fichier=?";
$stmt = $db->prepare($requete);
$stmt->bind_param('iisss', $id_bateau_final, $id_generation, $statut, $detail_erreur, $nom_fichier);
}
$ok = $stmt->execute();
$stmt->close();
return $ok;
}
// ----------------------------------------------------------------------------
// Reconstruit l'adresse MAC (format XX:XX:XX:XX:XX:XX) à partir des 12
// premiers caractères hexadécimaux du nom de fichier.
// ----------------------------------------------------------------------------
function Adresse_mac_depuis_nom($nom_fichier)
{
preg_match('/^([0-9A-Fa-f]{12})_/', $nom_fichier, $m);
return implode(':', str_split(strtoupper($m[1]), 2));
}
function Executer_cron_quarantaine()
{
Ouverture_bdd("127.0.0.1:3307", BDD_USER, BDD_PASSWORD, BDD_DATABASE);
$db = $_SESSION['db'];
if (!is_dir(REPERTOIRE_QUARANTAINE))
{
echo "Répertoire de quarantaine introuvable (" . REPERTOIRE_QUARANTAINE . ") - rien à faire.\n";
Fermeture_bdd();
return;
}
$fichiers = array_values(array_filter(scandir(REPERTOIRE_QUARANTAINE),
fn($f) => preg_match('/^[0-9A-Fa-f]{12}_\d{6}_\d{6}_[0-9A-Fa-f]{16}\.sql\.gz\.enc$/', $f)));
echo count($fichiers) . " archive(s) en quarantaine à réexaminer.\n\n";
foreach ($fichiers as $nom)
{
$chemin = REPERTOIRE_QUARANTAINE . "/" . $nom;
$ligne = Lire_ligne_en_attente($db, $nom);
if ($ligne === null)
{
echo " - {$nom} : aucune ligne TB_Archives_traitees en attente trouvée - incohérence, à examiner manuellement.\n";
continue;
}
$adresse_mac = Adresse_mac_depuis_nom($nom);
$contenu_chiffre = file_get_contents($chemin);
$affectation = Resoudre_affectation_active($db, $adresse_mac);
if (!$affectation['existe'])
{
echo " - {$nom} : toujours aucune affectation active - reste en quarantaine.\n";
continue;
}
$id_bateau = $affectation['id_bateau'];
$dechiffrement = Decrypter_archive($contenu_chiffre);
if (!$dechiffrement['ok'])
{
// Un échec de déchiffrement ne se résoudra pas en attendant (contrairement à l'affectation ou
// au MMSI, qui dépendent d'une action administrative future) - rejet direct, comme
// cron_archivage.php (cf synthèse §38) :
echo " - {$nom} : ÉCHEC de déchiffrement (" . $dechiffrement['raison'] . ") - rejetée.\n";
Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', "Échec de déchiffrement : " . $dechiffrement['raison']);
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
rename($chemin, REPERTOIRE_ERREURS . "/" . $nom);
continue;
}
$mmsi_recu = $dechiffrement['entete']['No_mmsi'] ?? null;
$mmsi_attendu_connu = null;
if ($mmsi_recu !== null)
{
$verification_mmsi = Verifier_mmsi($db, $id_bateau, $mmsi_recu);
$mmsi_attendu_connu = $verification_mmsi['attendu'];
if ($verification_mmsi['statut'] === 'quarantaine')
{
echo " - {$nom} : MMSI toujours divergent ({$mmsi_recu} / {$verification_mmsi['attendu']}) - reste en quarantaine.\n";
continue;
}
}
// La cause de la quarantaine est levée : traitement complet, même déroulé que cron_archivage.php :
$origine_bdd = $dechiffrement['entete']['Origine_bdd'] ?? 'normale';
$version_cible = $dechiffrement['entete']['Version_soft_no_package'] ?? null;
$generation_active = Resoudre_generation_active($db, $id_bateau);
$nouvelle_generation = Nouvelle_generation_necessaire($origine_bdd, $generation_active);
$fichier_sql = Resoudre_fichier_sql($db, $version_cible);
if ($fichier_sql === false)
{
echo " - {$nom} : version logicielle '{$version_cible}' inconnue de TB_Versions - rejetée.\n";
Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', "Version logicielle '{$version_cible}' inconnue de TB_Versions", $mmsi_recu, $mmsi_attendu_connu);
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
rename($chemin, REPERTOIRE_ERREURS . "/" . $nom);
continue;
}
$contenu_fichier_sql = @file_get_contents(REPERTOIRE_VERSIONS_SQL . "/" . $fichier_sql);
if ($contenu_fichier_sql === false)
{
echo " - {$nom} : fichier {$fichier_sql} introuvable sur le VPS - rejetée.\n";
Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', "Fichier {$fichier_sql} introuvable sur le VPS", $mmsi_recu, $mmsi_attendu_connu);
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
rename($chemin, REPERTOIRE_ERREURS . "/" . $nom);
continue;
}
if ($nouvelle_generation)
{
$zone_usine = Extraire_zone_usine($contenu_fichier_sql);
if ($zone_usine === false)
{
echo " - {$nom} : marqueur Factory_structure introuvable dans {$fichier_sql} - rejetée.\n";
Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', "Marqueur Factory_structure introuvable dans {$fichier_sql}", $mmsi_recu, $mmsi_attendu_connu);
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
rename($chemin, REPERTOIRE_ERREURS . "/" . $nom);
continue;
}
$motif = ($origine_bdd === 'factory' || $origine_bdd === 'restauration') ? $origine_bdd : 'creation_initiale';
$resultat_generation = Creer_generation($db, $id_bateau, $motif, $zone_usine);
if (!$resultat_generation['ok'])
{
echo " - {$nom} : échec de création de génération : " . $resultat_generation['raison'] . " - rejetée.\n";
Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', "Échec de création de génération : " . $resultat_generation['raison'], $mmsi_recu, $mmsi_attendu_connu);
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
rename($chemin, REPERTOIRE_ERREURS . "/" . $nom);
continue;
}
$nom_base_cible = $resultat_generation['nom_base'];
}
else
{
$nom_base_cible = $generation_active['nom_base'];
}
$chargement = Charger_contenu_archive($nom_base_cible, $dechiffrement['sql']);
if (!$chargement['ok'])
{
echo " - {$nom} : échec de chargement dans {$nom_base_cible} : " . $chargement['raison'] . " - rejetée.\n";
Trancher_archive_en_attente($db, $nom, $id_bateau, null, 'rejetee', "Échec de chargement dans {$nom_base_cible} : " . $chargement['raison'], $mmsi_recu, $mmsi_attendu_connu);
if (!is_dir(REPERTOIRE_ERREURS)) @mkdir(REPERTOIRE_ERREURS, 0750, true);
rename($chemin, REPERTOIRE_ERREURS . "/" . $nom);
continue;
}
$generation_pour_enregistrement = Resoudre_generation_active($db, $id_bateau);
Trancher_archive_en_attente($db, $nom, $id_bateau, $generation_pour_enregistrement['cle'], 'traitee', null, $mmsi_recu, $mmsi_attendu_connu);
unlink($chemin);
echo " - {$nom} : quarantaine levée, chargée dans {$nom_base_cible}.\n";
}
Fermeture_bdd();
}
Executer_cron_quarantaine();
+362
View File
@@ -0,0 +1,362 @@
<?php
// ============================================================================
// fonctions_archivage.php - Fonctions communes à cron_archivage.php (traitement
// initial des archives reçues des CM5) et cron_quarantaine.php (réexamen des
// archives mises en quarantaine) - cf synthèse §32-39.
//
// Nécessite d'avoir déjà inclus config/config.php et config/fonctions_bdd.php,
// et que CLE_CHIFFREMENT_ARCHIVE, BDD_BATEAUX_HOST/PORT/USER/PASSWORD soient
// définis (via config/confidentiel.php).
// ============================================================================
// ----------------------------------------------------------------------------
// Répertoires partagés entre les deux scripts (cf synthèse §38) :
// - REPERTOIRE_VERSIONS_SQL : fichiers de migration SQL (lecture seule).
// - REPERTOIRE_QUARANTAINE : archives en attente d'une décision administrative
// (MMSI divergent, ou aucune affectation active) - écrit par
// cron_archivage.php, lu ET écrit par cron_quarantaine.php.
// - REPERTOIRE_ERREURS : archives rejetées suite à une erreur technique non
// transitoire - écrit par les deux scripts, à usage de suivi manuel.
// ----------------------------------------------------------------------------
const REPERTOIRE_VERSIONS_SQL = "/home/koriolan/versions/shipheart/ressources/sql";
const REPERTOIRE_QUARANTAINE = "/home/koriolan/bateaux/archives/quarantaines";
const REPERTOIRE_ERREURS = "/home/koriolan/bateaux/archives/erreurs";
// ----------------------------------------------------------------------------
// Résout l'affectation active d'un boîtier (par adresse MAC) - cf synthèse
// §38. Renvoie ['existe' => bool, 'id_bateau' => int|null]. 'existe' => false
// couvre deux cas distincts à cet endroit, traités de la même façon : boîtier
// jamais affecté (première connexion), ou affecté par le passé puis retiré
// (toutes ses lignes ont une Date_fin renseignée).
// ----------------------------------------------------------------------------
function Resoudre_affectation_active($db, $adresse_mac)
{
$requete = "SELECT ID_Bateau FROM TB_Affectations_bateaux WHERE Adresse_mac=? AND Date_fin IS NULL";
$stmt = $db->prepare($requete);
$stmt->bind_param('s', $adresse_mac);
$stmt->execute();
$stmt->bind_result($id_bateau);
$trouve = $stmt->fetch();
$stmt->close();
if (!$trouve) return ['existe' => false, 'id_bateau' => null];
return ['existe' => true, 'id_bateau' => $id_bateau];
}
// ----------------------------------------------------------------------------
// Déchiffre + décompresse le contenu brut d'une archive (même chaîne openssl,
// exactement, que côté CM5 - cf synthèse §33.2), puis extrait les champs de
// son en-tête (# [Champ=Valeur]). Renvoie ['ok'=>true, 'entete'=>[...],
// 'sql'=>string] ou ['ok'=>false, 'raison'=>string].
// Nécessite CLE_CHIFFREMENT_ARCHIVE (config/confidentiel.php) - même valeur,
// exactement, que côté CM5 (cf synthèse §35.6).
// ----------------------------------------------------------------------------
function Decrypter_archive($contenu_chiffre)
{
$fichier_enc = tempnam(sys_get_temp_dir(), 'arch_enc_');
$fichier_gz = $fichier_enc . '.gz';
file_put_contents($fichier_enc, $contenu_chiffre);
$commande = "openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -md sha256 -d -in " . escapeshellarg($fichier_enc)
. " -out " . escapeshellarg($fichier_gz) . " -pass pass:" . escapeshellarg(CLE_CHIFFREMENT_ARCHIVE) . " 2>&1";
exec($commande, $sortie, $code_retour);
if ($code_retour !== 0)
{
unlink($fichier_enc);
return ['ok' => false, 'raison' => "échec du déchiffrement"];
}
$sql = @shell_exec("gzip -dc " . escapeshellarg($fichier_gz) . " 2>&1");
unlink($fichier_enc);
@unlink($fichier_gz);
if ($sql === null || $sql === '')
{
return ['ok' => false, 'raison' => "échec de la décompression"];
}
// Lecture des lignes d'en-tête (uniquement les toutes premières lignes '# [Champ=Valeur]') :
$entete = [];
foreach (explode("\n", $sql) as $ligne)
{
if (!preg_match('/^#\s*\[([A-Za-z_]+)=(.*)\]\s*$/', $ligne, $m)) break; // fin de l'en-tête dès la 1ère ligne qui ne correspond plus
$entete[$m[1]] = $m[2];
}
return ['ok' => true, 'entete' => $entete, 'sql' => $sql];
}
// ----------------------------------------------------------------------------
// 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'=>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";
$stmt = $db->prepare($requete);
$stmt->bind_param('i', $id_bateau);
$stmt->execute();
$stmt->bind_result($cle, $numero, $nom_base);
$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];
}
// ----------------------------------------------------------------------------
// Détermine si cette archive doit démarrer une NOUVELLE génération (§35.5) :
// - 'factory' ou 'restauration' dans l'en-tête -> toujours une nouvelle
// génération, quelle que soit la génération active actuelle.
// - 'normale' mais aucune génération active -> tout premier bateau,
// création de la génération 1 (même traitement que factory/restauration
// à partir de ce point, cf synthèse §35.5).
// - 'normale' avec une génération active -> pas de nouvelle génération,
// traitement incrémental habituel dans la génération existante.
// ----------------------------------------------------------------------------
function Nouvelle_generation_necessaire($origine_bdd, $generation_active)
{
if ($origine_bdd === 'factory' || $origine_bdd === 'restauration') return true;
return !$generation_active['existe'];
}
// ----------------------------------------------------------------------------
// Résout le nom du fichier de migration SQL correspondant à une version
// logicielle donnée (Version_soft_no_package, transmis dans l'en-tête de
// chaque archive - cf synthèse §32.2). Renvoie le nom de fichier (string),
// ou false si la version est inconnue ou la ligne TB_Versions_sql absente.
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Compare le MMSI reçu à celui déjà enregistré pour ce bateau (cf synthèse
// §32.3). Si TB_Bateaux.MMSI est NULL, la première valeur reçue est affectée
// automatiquement (aucun risque, rien à contredire). Renvoie :
// ['statut' => 'ok']
// ['statut' => 'quarantaine', 'attendu' => ..., 'recu' => ...]
// ----------------------------------------------------------------------------
function Verifier_mmsi($db_principale, $id_bateau, $mmsi_recu)
{
$requete = "SELECT MMSI FROM TB_Bateaux WHERE Cle_primaire=?";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('i', $id_bateau);
$stmt->execute();
$stmt->bind_result($mmsi_attendu);
$stmt->fetch();
$stmt->close();
if ($mmsi_attendu === null)
{
$requete = "UPDATE TB_Bateaux SET MMSI=? WHERE Cle_primaire=?";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('si', $mmsi_recu, $id_bateau);
$stmt->execute();
$stmt->close();
// Auto-affectation : l'attendu devient le reçu (rien à contredire) - toujours renseigné en sortie,
// jamais NULL, pour ne jamais laisser planer une ambiguïté entre "MMSI inconnu" et "MMSI connu mais
// pas indiqué ici" (cf synthèse §39) :
return ['statut' => 'ok', 'recu' => $mmsi_recu, 'attendu' => $mmsi_recu];
}
if ($mmsi_attendu === $mmsi_recu) return ['statut' => 'ok', 'recu' => $mmsi_recu, 'attendu' => $mmsi_attendu];
return ['statut' => 'quarantaine', 'attendu' => $mmsi_attendu, 'recu' => $mmsi_recu];
}
function Resoudre_fichier_sql($db, $version_package)
{
$requete = "SELECT vs.Fichier
FROM TB_Versions v
JOIN TB_Versions_sql vs ON vs.Cle_primaire = v.ID_Version_sql
WHERE v.Nom = ?";
$stmt = $db->prepare($requete);
$stmt->bind_param('s', $version_package);
$stmt->execute();
$stmt->bind_result($fichier);
$trouve = $stmt->fetch();
$stmt->close();
if (!$trouve) return false;
return $fichier;
}
// ----------------------------------------------------------------------------
// Remplace le nom de base littéral "shipheart" (utilisé dans Factory_structure
// pour le CM5 lui-même) par le vrai nom de la base par bateau/génération
// côté serveur (cf synthèse §35.4/36) - ancré précisément sur le contexte
// (DROP/CREATE DATABASE, USE), jamais un remplacement aveugle du mot : une
// valeur de configuration ("logo_shipheart.png") contient aussi ce mot sans
// aucun rapport avec un nom de base. Renvoie [contenu_adapte, nb_remplacements].
// ----------------------------------------------------------------------------
function Adapter_nom_base($contenu_sql, $nom_base_cible)
{
$nom_echappe = '`' . $nom_base_cible . '`';
$contenu_sql = preg_replace('/\bDATABASE IF EXISTS `?shipheart`?\b/', 'DATABASE IF EXISTS ' . $nom_echappe, $contenu_sql, -1, $n1);
$contenu_sql = preg_replace('/\bDATABASE IF NOT EXISTS `?shipheart`?\b/', 'DATABASE IF NOT EXISTS ' . $nom_echappe, $contenu_sql, -1, $n2);
$contenu_sql = preg_replace('/\bUSE `?shipheart`?\s*;/', 'USE ' . $nom_echappe . ';', $contenu_sql, -1, $n3);
return [$contenu_sql, $n1 + $n2 + $n3];
}
// ----------------------------------------------------------------------------
// Crée une nouvelle génération pour un bateau (cf synthèse §35.4/35.5/36) :
// calcule le nom de la nouvelle base, adapte et exécute le SQL fourni (déjà
// résolu par Extraire_zone_usine), clôture l'ancienne génération active si
// elle existe (jamais supprimée), puis enregistre la nouvelle dans
// TB_Generations_bateaux. Renvoie ['ok'=>bool, 'nom_base'=>string, ...].
// Exécution via le client mysql en ligne de commande (comme côté CM5,
// 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)
{
$generation_actuelle = Resoudre_generation_active($db_principale, $id_bateau);
$prochain_numero = $generation_actuelle['existe'] ? $generation_actuelle['numero'] + 1 : 1;
$nom_base = "ship2net_bateau_{$id_bateau}_{$prochain_numero}";
[$sql_adapte, $nb_remplacements] = Adapter_nom_base($contenu_sql, $nom_base);
if ($nb_remplacements < 1)
{
return ['ok' => false, 'raison' => "aucune substitution de nom de base effectuée - fichier SQL probablement mal formé"];
}
$fichier_temp = tempnam(sys_get_temp_dir(), 'usine_');
file_put_contents($fichier_temp, $sql_adapte);
$commande = "mysql --host=" . escapeshellarg(BDD_BATEAUX_HOST) . " --port=" . escapeshellarg(BDD_BATEAUX_PORT)
. " --user=" . escapeshellarg(BDD_BATEAUX_USER) . " --password=" . escapeshellarg(BDD_BATEAUX_PASSWORD)
. " < " . escapeshellarg($fichier_temp) . " 2>&1";
exec($commande, $sortie, $code_retour);
unlink($fichier_temp);
if ($code_retour !== 0)
{
return ['ok' => false, 'raison' => "échec de l'exécution SQL : " . implode(" | ", $sortie)];
}
if ($generation_actuelle['existe'])
{
$requete = "UPDATE TB_Generations_bateaux SET Date_fin=NOW() WHERE Cle_primaire=?";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('i', $generation_actuelle['cle']);
$stmt->execute();
$stmt->close();
}
$requete = "INSERT INTO TB_Generations_bateaux (ID_Bateau, Numero_generation, Nom_base, Motif) VALUES (?, ?, ?, ?)";
$stmt = $db_principale->prepare($requete);
$stmt->bind_param('iiss', $id_bateau, $prochain_numero, $nom_base, $motif);
if (!$stmt->execute())
{
$erreur = $db_principale->error;
$stmt->close();
return ['ok' => false, 'raison' => "base créée mais son enregistrement dans TB_Generations_bateaux a échoué : " . $erreur];
}
$stmt->close();
return ['ok' => true, 'nom_base' => $nom_base, 'numero' => $prochain_numero];
}
// ----------------------------------------------------------------------------
// Extrait la zone "usine" d'un fichier de migration (Factory_structure +
// Factory_data ensemble, jusqu'à la fin du fichier) - pour une génération
// neuve (cf synthèse §32.2, §35.5). Renvoie false si le marqueur est
// introuvable (fichier mal formé - ne devrait jamais arriver en pratique).
// ----------------------------------------------------------------------------
function Extraire_zone_usine($contenu_fichier)
{
if (!preg_match('/^#\s*Factory_structure\b/m', $contenu_fichier, $m, PREG_OFFSET_CAPTURE)) return false;
return substr($contenu_fichier, $m[0][1]);
}
// ----------------------------------------------------------------------------
// Extrait les segments incrémentaux entre deux versions (exclusive pour le
// départ, inclusive pour la cible) - pour une mise à jour incrémentale d'une
// génération déjà existante (cf synthèse §32.2). $depart et $cible sont des
// noms de fichier ("shipheart_sql_AAMMJJ_HHMMSS.sql"), résolus au préalable
// via TB_Versions/TB_Versions_sql (bloc 4a). Si $depart === $cible, renvoie
// une chaîne vide (rien à migrer - cas normal si le bateau n'a pas été mis à
// jour depuis la dernière archive). Renvoie false si l'un des deux marqueurs
// est introuvable.
// ----------------------------------------------------------------------------
function Extraire_segments_incrementaux($contenu_fichier, $depart, $cible)
{
if ($depart === $cible) return '';
$motif_depart = '/^#\s*Fin de ' . preg_quote($depart, '/') . '\b.*$/m';
if (!preg_match($motif_depart, $contenu_fichier, $m_depart, PREG_OFFSET_CAPTURE)) return false;
$debut = $m_depart[0][1] + strlen($m_depart[0][0]);
$motif_cible = '/^#\s*Fin de ' . preg_quote($cible, '/') . '\b.*$/m';
if (!preg_match($motif_cible, $contenu_fichier, $m_cible, PREG_OFFSET_CAPTURE)) return false;
$fin = $m_cible[0][1] + strlen($m_cible[0][0]);
if ($fin <= $debut) return false; // Ordre incohérent (cible avant départ) - ne devrait jamais arriver, "pas d'update en arrière" (§32.2)
return substr($contenu_fichier, $debut, $fin - $debut);
}
// ----------------------------------------------------------------------------
// 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,
// cf sauvegarde_bdd()/bdd_admin.c) - jamais une liste codée en dur, pour rester
// robuste si une nouvelle table TB_Mesures_xxx apparaît un jour.
// ----------------------------------------------------------------------------
function Resoudre_tables_fragmentees($connexion_bateau)
{
$tables = ['TB_Historique'];
$resultat = $connexion_bateau->query("SHOW TABLES LIKE 'TB_Mesures%'");
while ($ligne = $resultat->fetch_row()) $tables[] = $ligne[0];
return $tables;
}
// ----------------------------------------------------------------------------
// Charge le contenu réel d'une archive (déjà déchiffré) dans la base d'une
// génération : les tables permanentes sont vidées puis rechargées, les tables
// incrémentales (fragmentées) sont uniquement complétées (jamais vidées) -
// cf synthèse §32/37. Le contenu de l'archive (ses INSERT) complète donc
// naturellement les tables fragmentées et remplace les permanentes, sans
// avoir besoin de séparer les deux : seul le DELETE préalable fait la
// différence entre les deux comportements. Exécution via mysql CLI (comme
// Creer_generation), SET FOREIGN_KEY_CHECKS=0 par précaution (l'ordre des
// INSERT du dump ne respecte pas forcément les dépendances).
// ----------------------------------------------------------------------------
function Charger_contenu_archive($nom_base, $sql_archive)
{
$connexion_bateau = new mysqli(BDD_BATEAUX_HOST, BDD_BATEAUX_USER, BDD_BATEAUX_PASSWORD, $nom_base, (int)BDD_BATEAUX_PORT);
if ($connexion_bateau->connect_error)
{
return ['ok' => false, 'raison' => "connexion à {$nom_base} impossible : " . $connexion_bateau->connect_error];
}
$tables_fragmentees = Resoudre_tables_fragmentees($connexion_bateau);
$resultat_tables = $connexion_bateau->query("SHOW TABLES");
$toutes_tables = [];
while ($ligne = $resultat_tables->fetch_row()) $toutes_tables[] = $ligne[0];
$tables_permanentes = array_diff($toutes_tables, $tables_fragmentees);
$connexion_bateau->close();
$script = "SET FOREIGN_KEY_CHECKS=0;\n";
foreach ($tables_permanentes as $table)
{
$script .= "DELETE FROM `{$table}`;\n";
}
$script .= $sql_archive . "\n";
$script .= "SET FOREIGN_KEY_CHECKS=1;\n";
$fichier_temp = tempnam(sys_get_temp_dir(), 'charge_');
file_put_contents($fichier_temp, $script);
$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' => "échec du chargement : " . implode(" | ", $sortie)];
}
return ['ok' => true];
}
+7
View File
@@ -0,0 +1,7 @@
-- ============================================================================
-- Ajout de Detail_erreur (cf synthèse §38) : trace la cause d'un rejet ou
-- d'une quarantaine directement en base, pour un suivi manuel sans dépendre
-- des journaux de la tâche cron (volume négligeable, usage rare).
-- ============================================================================
ALTER TABLE `TB_Archives_traitees`
ADD COLUMN `Detail_erreur` TEXT NULL COMMENT 'Détail de l''erreur technique ayant mené à Statut=rejetee (ou tout autre statut, si utile)' AFTER `MMSI_attendu`;
+15
View File
@@ -0,0 +1,15 @@
-- ============================================================================
-- Renommages de cohérence (cf synthèse §38) :
-- - TB_Affectations_bateau -> TB_Affectations_bateaux
-- - TB_Generations_bateau -> TB_Generations_bateaux
-- - TB_Archives_traitees.ID_Bateau_resolu -> ID_Bateau_affecte (pour ne plus
-- confondre avec Date_resolution, qui porte un sens différent : la date
-- à laquelle le STATUT a été tranché, pas la date de résolution de
-- l'affectation elle-même)
-- ============================================================================
RENAME TABLE `TB_Affectations_bateau` TO `TB_Affectations_bateaux`;
RENAME TABLE `TB_Generations_bateau` TO `TB_Generations_bateaux`;
ALTER TABLE `TB_Archives_traitees`
CHANGE COLUMN `ID_Bateau_resolu` `ID_Bateau_affecte` INT UNSIGNED NULL
COMMENT 'Déduit de TB_Affectations_bateaux à la réception - NULL si aucune affectation active trouvée';