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:
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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`;
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user