mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-15 19:28:16 +02:00
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>
136 lines
4.9 KiB
Rust
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);
|
|
}
|
|
}
|