mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-22 22:48:04 +02:00
Déploiement : versionner les JS pour contourner le cache du proxy
main.dart.js, flutter_bootstrap.js et xf-player-core.js gardent le même nom d'une build à l'autre. En prod, Nginx Proxy Manager les met en cache (Cache Assets) jusqu'à 12 h, même sur rechargement forcé : après le dernier déploiement, l'ancienne app et l'ancien lecteur tournaient avec le nouveau serveur. Au démarrage, le serveur réécrit index.html, flutter_bootstrap.js et player*.html pour référencer ces fichiers avec ?v=<empreinte du contenu> : chaque version a sa propre URL, aucun cache ne peut les confondre. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
662e1034e5
commit
6212948ea5
3 files changed
+156
-1
No files matched your search
+25
-1
@@ -27,6 +27,7 @@ import 'middleware/auth_middleware.dart';
|
|||||||
import 'middleware/security_middleware.dart';
|
import 'middleware/security_middleware.dart';
|
||||||
import 'services/cleanup_service.dart';
|
import 'services/cleanup_service.dart';
|
||||||
import 'services/recording_scheduler.dart';
|
import 'services/recording_scheduler.dart';
|
||||||
|
import 'utils/asset_versioning.dart';
|
||||||
|
|
||||||
void main(List<String> args) async {
|
void main(List<String> args) async {
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
@@ -293,14 +294,37 @@ void main(List<String> args) async {
|
|||||||
listDirectories: false,
|
listDirectories: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Points d'entrée réécrits avec `?v=<empreinte>` sur leurs JS : voir
|
||||||
|
// asset_versioning.dart (un proxy qui cache les .js servait l'ancienne
|
||||||
|
// app et l'ancien lecteur après chaque déploiement).
|
||||||
|
final versionedEntryPoints = buildVersionedEntryPoints(webPath);
|
||||||
|
print('[Server] Assets versionnés : ${versionedEntryPoints.keys.join(', ')}');
|
||||||
|
|
||||||
// Wrap static handler to enforce cache policies
|
// Wrap static handler to enforce cache policies
|
||||||
FutureOr<Response> staticHandler(Request request) async {
|
FutureOr<Response> staticHandler(Request request) async {
|
||||||
|
final path = request.url.path;
|
||||||
|
|
||||||
|
final versioned =
|
||||||
|
versionedEntryPoints[path.isEmpty ? 'index.html' : path];
|
||||||
|
if (versioned != null && request.method == 'GET') {
|
||||||
|
return Response.ok(
|
||||||
|
versioned,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': path.endsWith('.js')
|
||||||
|
? 'text/javascript; charset=utf-8'
|
||||||
|
: 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final response = await baseStaticHandler(request);
|
final response = await baseStaticHandler(request);
|
||||||
|
|
||||||
// Disable cache for entry points to ensure updates are seen immediately.
|
// Disable cache for entry points to ensure updates are seen immediately.
|
||||||
// Vendored player libraries (hls.js/mpegts.js, ~750 KB) are pinned
|
// Vendored player libraries (hls.js/mpegts.js, ~750 KB) are pinned
|
||||||
// versions: keep them cacheable or every player open re-downloads them.
|
// versions: keep them cacheable or every player open re-downloads them.
|
||||||
final path = request.url.path;
|
|
||||||
final isVendored = path.startsWith('vendor/');
|
final isVendored = path.startsWith('vendor/');
|
||||||
if (!isVendored &&
|
if (!isVendored &&
|
||||||
(path.isEmpty ||
|
(path.isEmpty ||
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import '../utils/asset_versioning.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('versionAssetRefs', () {
|
||||||
|
test('versionne la balise script du lecteur', () {
|
||||||
|
expect(
|
||||||
|
versionAssetRefs(
|
||||||
|
'<script src="xf-player-core.js"></script>',
|
||||||
|
{'xf-player-core.js': 'abc123'},
|
||||||
|
),
|
||||||
|
'<script src="xf-player-core.js?v=abc123"></script>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versionne mainJsPath dans la config de build Flutter', () {
|
||||||
|
expect(
|
||||||
|
versionAssetRefs(
|
||||||
|
'{"compileTarget":"dart2js","mainJsPath":"main.dart.js"}',
|
||||||
|
{'main.dart.js': 'f00'},
|
||||||
|
),
|
||||||
|
'{"compileTarget":"dart2js","mainJsPath":"main.dart.js?v=f00"}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ne touche ni un nom qui ne fait que contenir l\'asset ni une ref déjà versionnée', () {
|
||||||
|
const html = '<script src="vendor/xf-player-core.js"></script>'
|
||||||
|
'<script src="xf-player-core.js?v=old"></script>';
|
||||||
|
expect(versionAssetRefs(html, {'xf-player-core.js': 'new'}), html);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guillemets simples acceptés', () {
|
||||||
|
expect(
|
||||||
|
versionAssetRefs("<link href='xf-player.css'>", {'xf-player.css': '1'}),
|
||||||
|
"<link href='xf-player.css?v=1'>",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('contentVersion', () {
|
||||||
|
test('stable pour un même contenu, différente sinon', () {
|
||||||
|
expect(contentVersion([1, 2, 3]), contentVersion([1, 2, 3]));
|
||||||
|
expect(contentVersion([1, 2, 3]), isNot(contentVersion([1, 2, 4])));
|
||||||
|
expect(contentVersion([1, 2, 3]), hasLength(12));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
|
||||||
|
/// Empreinte courte et stable de [bytes].
|
||||||
|
String contentVersion(List<int> bytes) =>
|
||||||
|
md5.convert(bytes).toString().substring(0, 12);
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
String versionAssetRefs(String content, Map<String, String> versions) {
|
||||||
|
var out = content;
|
||||||
|
versions.forEach((asset, version) {
|
||||||
|
final pattern = RegExp('(["\'])${RegExp.escape(asset)}\\1');
|
||||||
|
out = out.replaceAllMapped(
|
||||||
|
pattern,
|
||||||
|
(m) => '${m[1]}$asset?v=$version${m[1]}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return 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.
|
||||||
|
Map<String, String> buildVersionedEntryPoints(String webPath) {
|
||||||
|
String? read(String name) {
|
||||||
|
final file = File('$webPath/$name');
|
||||||
|
return file.existsSync() ? file.readAsStringSync() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? versionOf(String name) {
|
||||||
|
final file = File('$webPath/$name');
|
||||||
|
return file.existsSync() ? contentVersion(file.readAsBytesSync()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rewritten = <String, String>{};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
final bootstrap = read('flutter_bootstrap.js');
|
||||||
|
final mainVersion = versionOf('main.dart.js');
|
||||||
|
if (bootstrap != null && mainVersion != null) {
|
||||||
|
final versioned =
|
||||||
|
versionAssetRefs(bootstrap, {'main.dart.js': mainVersion});
|
||||||
|
rewritten['flutter_bootstrap.js'] = versioned;
|
||||||
|
final index = read('index.html');
|
||||||
|
if (index != null) {
|
||||||
|
rewritten['index.html'] = versionAssetRefs(index, {
|
||||||
|
'flutter_bootstrap.js': contentVersion(utf8.encode(versioned)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final playerAssets = <String, String>{
|
||||||
|
for (final name in const ['xf-player-core.js', 'xf-player.css'])
|
||||||
|
if (versionOf(name) case final v?) name: v,
|
||||||
|
};
|
||||||
|
for (final page in const [
|
||||||
|
'player.html',
|
||||||
|
'player_lite.html',
|
||||||
|
'player_mobile.html',
|
||||||
|
]) {
|
||||||
|
final html = read(page);
|
||||||
|
if (html != null) rewritten[page] = versionAssetRefs(html, playerAssets);
|
||||||
|
}
|
||||||
|
return rewritten;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user