diff --git a/server/admin/versions.php b/server/admin/versions.php
index 607db18..c42def6 100644
--- a/server/admin/versions.php
+++ b/server/admin/versions.php
@@ -283,9 +283,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
$manifest = loadManifest($manifestPath);
-$zips = is_dir($buildsDir) ? array_map('basename', glob("$buildsDir/*.zip") ?: []) : [];
+// Liste des ZIPs : on inclut builds/ + builds/{channel}/ (1 niveau de
+// sous-dossier) pour lister aussi les ZIPs d'un channel spécifique. Les
+// chemins affichés sont relatifs à $buildsDir pour distinguer les ZIPs
+// "racine" (default) des ZIPs en sous-dossier (asterion-vr/proserve-X.zip).
+$zips = [];
$zipSizes = [];
-foreach ($zips as $z) $zipSizes[$z] = filesize("$buildsDir/$z");
+if (is_dir($buildsDir)) {
+ foreach (array_merge(
+ glob("$buildsDir/*.zip") ?: [],
+ glob("$buildsDir/*/*.zip") ?: []
+ ) as $z) {
+ $rel = ltrim(str_replace($buildsDir, '', $z), '/\\');
+ $rel = str_replace('\\', '/', $rel);
+ $zips[] = $rel;
+ $zipSizes[$rel] = filesize($z);
+ }
+}
// Pour l'édition : pré-charge les release notes existantes
$existingNotes = [];
@@ -342,7 +356,15 @@ Layout::header('Versions', 'versions');
Workflow d'une nouvelle release
- Ajoute l'entrée du manifest avec le formulaire ci-dessous (version, date min de license, release notes).
- - Upload le ZIP correspondant via SFTP dans
www/PS_Launcher/builds/ en respectant le nom proserve-{version}.zip.
+ - Upload le ZIP correspondant via SFTP en respectant le nom
proserve-{version}.zip, dans le dossier qui correspond au channel actif :
+
+
+ - Channel default →
www/PS_Launcher/builds/proserve-{version}.zip
+
+ - Channel = htmlspecialchars($channel) ?> →
www/PS_Launcher/builds/= htmlspecialchars($channel) ?>/proserve-{version}.zip (crée le sous-dossier en SFTP s'il n'existe pas)
+
+
+
- Clique 🔁 Sync (sign-manifest) pour calculer le SHA-256, mettre à jour
sizeBytes, bumper latest, et signer le manifest avec Ed25519.
- Les clients PS_Launcher détecteront la nouvelle version au prochain « Vérifier les MAJ ».
@@ -581,12 +603,18 @@ Layout::header('Versions', 'versions');
| Fichier | Taille | Référencé ? |
= htmlspecialchars($z) ?> |
diff --git a/server/tools/SignManifest.php b/server/tools/SignManifest.php
index fa6fe62..32f07a9 100644
--- a/server/tools/SignManifest.php
+++ b/server/tools/SignManifest.php
@@ -41,6 +41,32 @@ final class SignManifest
private function out(string $line): void { $this->log[] = $line; }
+ /**
+ * Mappe le chemin URL d'un ZIP (ex. /PS_Launcher/builds/asterion-vr/proserve-1.4.6.zip)
+ * vers son chemin local sur disque (ex. {$buildsDir}/asterion-vr/proserve-1.4.6.zip).
+ *
+ * Stratégie : on cherche le segment '/builds/' dans le URL path et on prend
+ * tout ce qui vient après comme chemin relatif depuis $buildsDir. Sécurité :
+ * on neutralise '..' pour empêcher path traversal, même si l'URL est déjà
+ * sous notre contrôle (manifest signé Ed25519).
+ *
+ * Si le pattern '/builds/' n'est pas trouvé, fallback sur basename comme avant
+ * (compat ascendante avec d'éventuelles URLs custom).
+ */
+ private function resolveZipPath(string $urlPath): string
+ {
+ $marker = '/builds/';
+ $pos = strpos($urlPath, $marker);
+ if ($pos === false) {
+ return "{$this->buildsDir}/" . basename($urlPath);
+ }
+ $relative = substr($urlPath, $pos + strlen($marker));
+ // Sécu : strip '..' segments pour éviter path traversal hors de buildsDir
+ $relative = str_replace(['..\\', '../', '..'], '', $relative);
+ $relative = ltrim($relative, '/\\');
+ return "{$this->buildsDir}/$relative";
+ }
+
/**
* Lit le cache des hashs précalculés. Retourne un dictionnaire
* [path => ['size' => int, 'mtime' => int, 'sha256' => string]].
@@ -150,19 +176,29 @@ final class SignManifest
continue;
}
- $filename = basename(parse_url($url, PHP_URL_PATH) ?: '');
- $zip = "{$this->buildsDir}/$filename";
+ // Le ZIP peut vivre dans builds/ directement (channel default) OU dans
+ // builds/{channel}/ pour les autres channels. On extrait donc TOUT le
+ // chemin après /builds/ dans l'URL plutôt qu'un simple basename().
+ $urlPath = parse_url($url, PHP_URL_PATH) ?: '';
+ $filename = basename($urlPath);
+ $zip = $this->resolveZipPath($urlPath);
if (!is_file($zip)) {
- $candidates = glob("{$this->buildsDir}/*{$version}*.zip", GLOB_NOSORT) ?: [];
+ // Fallback : recherche fuzzy sur la version dans builds/ + sous-dossiers
+ // immédiats (1 niveau) au cas où l'admin aurait renommé le ZIP.
+ $candidates = array_merge(
+ glob("{$this->buildsDir}/*{$version}*.zip", GLOB_NOSORT) ?: [],
+ glob("{$this->buildsDir}/*/*{$version}*.zip", GLOB_NOSORT) ?: []
+ );
$candidates = array_values(array_filter($candidates, 'is_file'));
if (count($candidates) === 1) {
$zip = $candidates[0];
- $this->out(" [info] $version : URL pointait vers '{$filename}', utilisé '" . basename($zip) . "' à la place");
+ $this->out(" [info] $version : URL pointait vers '{$filename}', utilisé '" . str_replace($this->buildsDir . '/', '', $zip) . "' à la place");
} else {
$this->out(" [skip] $version : ZIP introuvable pour $url");
+ $this->out(" Cherché : " . str_replace($this->buildsDir . '/', '', $zip));
if (count($candidates) > 1) {
- $this->out(" Plusieurs candidats : " . implode(', ', array_map('basename', $candidates)));
+ $this->out(" Plusieurs candidats : " . implode(', ', array_map(fn($c) => str_replace($this->buildsDir . '/', '', $c), $candidates)));
}
continue;
}