sync_one : hash uniquement l'entrée cliquée, pas toutes celles du même numéro

Bug : sur un manifest avec deux entrées partageant le même numéro de version
(channels multiples — ex proserve-firefighter-1.5.4.32.zip ET proserve-full-
1.5.4.32.zip, deux builds distincts pour firefighter vs full), cliquer le
bouton « 🔁 Hash » d'une ligne déclenchait le hash des DEUX ZIPs dans la
même requête HTTP. Résultat 2 × 14 Go = 28 Go dans une seule requête, ce
qui dépasse le timeout front d'OVH mutualisé (hard-limit invisible côté
PHP même avec set_time_limit(0)) → 500 Internal Server Error.

C'est pour ça que « des hashs de la même taille passaient sans problème
avant » : c'est le cas 28 Go des DEUX ZIPs en une requête qui est nouveau
(depuis l'introduction des channels), pas la taille du single 14 Go qui
passait déjà.

Fix : nouveau paramètre $onlyEntryId sur SignManifest::run(). Prend le pas
sur $onlyVersion. L'admin action sync_one passe l'entryId (unique par
ligne, généré par generate_entry_id()) au lieu du numéro de version. Une
ligne cliquée = un ZIP hashé, point. Le bouton « 🔁 Sync (all versions) »
(action sync_versions) continue de tout hasher — c'est ce que l'opérateur
demande explicitement.

Rétro-compat : si $onlyEntryId est null (CLI, cron, anciens callers),
$onlyVersion filtre comme avant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 13:42:39 +02:00
parent aae339287b
commit b0ca082b52
2 changed files with 30 additions and 12 deletions

View File

@@ -671,11 +671,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_once "$root/tools/SignManifest.php"; require_once "$root/tools/SignManifest.php";
$signer = new \PSLauncher\Tools\SignManifest($root); $signer = new \PSLauncher\Tools\SignManifest($root);
$force = !empty($_POST['force']); $force = !empty($_POST['force']);
// SignManifest::run filtre par numéro de version ; en cas d'entries // Filtre par entryId (pas par numéro de version). Sinon, quand deux
// de même version sur des channels différents, le hash sera recalculé // entrées partagent le même numéro (channels firefighter vs full sur
// pour toutes celles qui matchent (chacune pointe sur son propre ZIP, // 1.5.4.32 p.ex.), les DEUX ZIPs sont hashés dans la même requête HTTP
// donc le résultat est correct, juste un peu plus de boulot). // → 2 × 14 Go = risque de dépasser le timeout front OVH sur mutualisé.
$result = $signer->run('versions', $force, $version); $result = $signer->run('versions', $force, null, $entryId);
$forceLabel = $force ? ' [FORCE]' : ''; $forceLabel = $force ? ' [FORCE]' : '';
$message = "Hash de v{$version}{$forceLabel} :\n" . implode("\n", $result['log']); $message = "Hash de v{$version}{$forceLabel} :\n" . implode("\n", $result['log']);
if (!$result['ok']) $messageType = 'error'; if (!$result['ok']) $messageType = 'error';

View File

@@ -139,10 +139,19 @@ final class SignManifest
* @param bool $force Si true, ignore le cache et recalcule tous les hashs * @param bool $force Si true, ignore le cache et recalcule tous les hashs
* @param ?string $onlyVersion Si non null, ne touche QUE cette version dans la section * @param ?string $onlyVersion Si non null, ne touche QUE cette version dans la section
* versions[] (les autres restent inchangées). Le scope 'launcher' * versions[] (les autres restent inchangées). Le scope 'launcher'
* est ignoré dans ce cas. * est ignoré dans ce cas. ATTENTION : si plusieurs entrées
* partagent le même numéro de version (channels firefighter
* vs full sur 1.5.4.32 p.ex.), TOUTES sont hashées — d'où
* $onlyEntryId ci-dessous pour cibler une seule ligne.
* @param ?string $onlyEntryId Si non null, ne touche QUE l'entrée avec cet id (généré par
* generate_entry_id() au backoffice). Prend le pas sur
* $onlyVersion. Utilisé par le bouton « 🔁 Hash » d'une
* ligne isolée pour éviter de re-hasher les autres channels
* qui partagent le même numéro de version (2 × 14 Go dans une
* requête HTTP → risque de timeout front OVH).
* @return array{ok:bool, log:string[]} * @return array{ok:bool, log:string[]}
*/ */
public function run(string $scope = 'all', bool $force = false, ?string $onlyVersion = null): array public function run(string $scope = 'all', bool $force = false, ?string $onlyVersion = null, ?string $onlyEntryId = null): array
{ {
// Sur OVH mutualisé, un SHA-256 d'un ZIP de 14 Go peut prendre 5-10 min via // Sur OVH mutualisé, un SHA-256 d'un ZIP de 14 Go peut prendre 5-10 min via
// le SAN partagé. Le max_execution_time par défaut (30-60s) tue le process // le SAN partagé. Le max_execution_time par défaut (30-60s) tue le process
@@ -166,7 +175,8 @@ final class SignManifest
$ts = date('Y-m-d H:i:s'); $ts = date('Y-m-d H:i:s');
@file_put_contents($errorLogPath, @file_put_contents($errorLogPath,
"[{$ts}] --- SignManifest::run(scope={$scope}, force=" . ($force?'1':'0') "[{$ts}] --- SignManifest::run(scope={$scope}, force=" . ($force?'1':'0')
. ", onlyVersion=" . ($onlyVersion ?? 'null') . ") ---\n", . ", onlyVersion=" . ($onlyVersion ?? 'null')
. ", onlyEntryId=" . ($onlyEntryId ?? 'null') . ") ---\n",
FILE_APPEND); FILE_APPEND);
// Fatal errors → capturés par un shutdown handler. Sinon Apache renvoie // Fatal errors → capturés par un shutdown handler. Sinon Apache renvoie
// juste 500 sans qu'on sache ce qui a claqué. // juste 500 sans qu'on sache ce qui a claqué.
@@ -193,9 +203,9 @@ final class SignManifest
$doVersions = ($scope === 'all' || $scope === 'versions'); $doVersions = ($scope === 'all' || $scope === 'versions');
$doLauncher = ($scope === 'all' || $scope === 'launcher'); $doLauncher = ($scope === 'all' || $scope === 'launcher');
if ($onlyVersion !== null) { if ($onlyVersion !== null || $onlyEntryId !== null) {
// Mode "hash une seule version" : on ne touche pas au launcher, // Mode "hash une seule entrée / version" : on ne touche pas au launcher,
// et on ne hash que la version demandée dans la section versions[]. // et on ne hash que ce qui correspond au filtre demandé dans versions[].
$doLauncher = false; $doLauncher = false;
$doVersions = true; $doVersions = true;
} }
@@ -207,7 +217,15 @@ final class SignManifest
$hashedVersions = []; $hashedVersions = [];
if ($doVersions) foreach ($manifest['versions'] as &$v) { if ($doVersions) foreach ($manifest['versions'] as &$v) {
$version = $v['version'] ?? '?'; $version = $v['version'] ?? '?';
if ($onlyVersion !== null && $version !== $onlyVersion) { $entryId = (string)($v['id'] ?? '');
// Filtre par id d'entrée prioritaire sur filtre par version (cas
// channels multiples partageant un numéro de version identique —
// ex : proserve-firefighter-1.5.4.32 vs proserve-full-1.5.4.32).
// Sinon, filtre par numéro de version (rétro-compat).
if ($onlyEntryId !== null && $entryId !== $onlyEntryId) {
continue;
}
if ($onlyEntryId === null && $onlyVersion !== null && $version !== $onlyVersion) {
continue; // skip silently les autres versions continue; // skip silently les autres versions
} }
$url = $v['download']['url'] ?? ''; $url = $v['download']['url'] ?? '';