Files
xtremflow/server/src/util/asset_versioning.rs
T
MichaelandClaude Opus 5.5 3ea6fa0ae3 Serveur réécrit en Rust
Le serveur Dart (bin/) est remplacé par un serveur Rust (server/) :
tokio + axum, reqwest/rustls, SQLite embarqué via rusqlite. Même API,
mêmes routes et mêmes réponses : le front Flutter ne change pas.

La base de production est reprise telle quelle : schéma, migrations et
formats de dates (DateTime.toIso8601String de Dart) sont reproduits à
l'identique, les sessions et hachages existants restent valides.

Comportement conservé : cascade de routage, anti-SSRF, logs masqués,
rate limits, transcodage HLS/turbo.ts avec repli GPU, quotas fournisseur,
enregistrements (reprise, relances, fusion), season passes, EPG, logos de
repli, arrêt gracieux sur SIGTERM. Tests Dart portés (132 tests Rust) ;
la CI exécute fmt, test et clippy -D warnings, l'image Docker compile le
serveur dans un étage rust:1.98.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:29:57 +02:00

136 lines
4.9 KiB
Rust

//! Versionnage des assets non hachés de la build web.
//!
//! POURQUOI : `main.dart.js`, `flutter_bootstrap.js` et `xf-player-core.js`
//! gardent le même nom d'une build à l'autre. Un reverse proxy qui met les
//! `.js` en cache (option « Cache Assets » de Nginx Proxy Manager, constatée
//! en prod : cache jusqu'à 12 h, même sur un rechargement forcé) servait donc
//! l'ANCIEN lecteur et l'ANCIENNE app avec le NOUVEAU serveur après chaque
//! déploiement. En ajoutant `?v=<empreinte du contenu>` aux références,
//! chaque version a sa propre URL : aucun cache intermédiaire ne peut la
//! confondre avec la précédente.
use md5::{Digest, Md5};
use std::collections::HashMap;
use std::path::Path;
/// Empreinte courte et stable de [bytes].
pub fn content_version(bytes: &[u8]) -> String {
let digest = Md5::digest(bytes);
let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
hex[..12].to_string()
}
/// Ajoute `?v=<version>` à chaque référence entre guillemets à un asset de
/// [versions] (`"main.dart.js"` → `"main.dart.js?v=…"`).
///
/// Seules les références exactes sont réécrites : `vendor/x.js` ou une
/// référence déjà versionnée restent intactes.
pub fn version_asset_refs(content: &str, versions: &[(&str, String)]) -> String {
let mut out = content.to_string();
for (asset, version) in versions {
for quote in ['"', '\''] {
let from = format!("{quote}{asset}{quote}");
let to = format!("{quote}{asset}?v={version}{quote}");
out = out.replace(&from, &to);
}
}
out
}
/// Contenus réécrits, indexés par chemin relatif (`index.html`…), prêts à
/// être servis à la place des fichiers d'origine.
///
/// Calculé une fois au démarrage : la build web est figée dans l'image.
pub fn build_versioned_entry_points(web_path: &str) -> HashMap<String, String> {
let base = Path::new(web_path);
let read = |name: &str| std::fs::read_to_string(base.join(name)).ok();
let version_of = |name: &str| {
std::fs::read(base.join(name))
.ok()
.map(|b| content_version(&b))
};
let mut rewritten = HashMap::new();
// Le bootstrap référence main.dart.js : sa version dépend donc de celle
// de main.dart.js, d'où une version calculée APRÈS réécriture.
if let (Some(bootstrap), Some(main_version)) =
(read("flutter_bootstrap.js"), version_of("main.dart.js"))
{
let versioned = version_asset_refs(&bootstrap, &[("main.dart.js", main_version)]);
let bootstrap_version = content_version(versioned.as_bytes());
rewritten.insert("flutter_bootstrap.js".to_string(), versioned);
if let Some(index) = read("index.html") {
rewritten.insert(
"index.html".to_string(),
version_asset_refs(&index, &[("flutter_bootstrap.js", bootstrap_version)]),
);
}
}
let player_assets: Vec<(&str, String)> = ["xf-player-core.js", "xf-player.css"]
.into_iter()
.filter_map(|name| version_of(name).map(|v| (name, v)))
.collect();
for page in ["player.html", "player_lite.html", "player_mobile.html"] {
if let Some(html) = read(page) {
rewritten.insert(page.to_string(), version_asset_refs(&html, &player_assets));
}
}
rewritten
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn versionne_la_balise_script_du_lecteur() {
assert_eq!(
version_asset_refs(
r#"<script src="xf-player-core.js"></script>"#,
&[("xf-player-core.js", "abc123".into())]
),
r#"<script src="xf-player-core.js?v=abc123"></script>"#
);
}
#[test]
fn versionne_main_js_path_dans_la_config_flutter() {
assert_eq!(
version_asset_refs(
r#"{"compileTarget":"dart2js","mainJsPath":"main.dart.js"}"#,
&[("main.dart.js", "f00".into())]
),
r#"{"compileTarget":"dart2js","mainJsPath":"main.dart.js?v=f00"}"#
);
}
#[test]
fn ne_touche_ni_un_nom_contenant_l_asset_ni_une_ref_versionnee() {
let html = r#"<script src="vendor/xf-player-core.js"></script><script src="xf-player-core.js?v=old"></script>"#;
assert_eq!(
version_asset_refs(html, &[("xf-player-core.js", "new".into())]),
html
);
}
#[test]
fn guillemets_simples_acceptes() {
assert_eq!(
version_asset_refs(
"<link href='xf-player.css'>",
&[("xf-player.css", "1".into())]
),
"<link href='xf-player.css?v=1'>"
);
}
#[test]
fn empreinte_stable() {
assert_eq!(content_version(&[1, 2, 3]), content_version(&[1, 2, 3]));
assert_ne!(content_version(&[1, 2, 3]), content_version(&[1, 2, 4]));
assert_eq!(content_version(&[1, 2, 3]).len(), 12);
}
}