From d5f36865b647461f22910f4174d9e29c36be9a8b Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Wed, 12 Aug 2026 17:47:16 +0200 Subject: [PATCH] =?UTF-8?q?D2=20:=20re-t=C3=A9l=C3=A9charger=20une=20resso?= =?UTF-8?q?urce=20p=C3=A9rim=C3=A9e,=20base=20locale=20v3=20vers=20v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le filtre incrémental testait la présence du fichier, jamais sa version. La prémisse d'origine était fausse, et ça change ce que le correctif répare : « une image remplacée dans le CMS ne remonte jamais » n'existe pas, parce que Upload appelle GenerateHexId() à chaque téléversement — remplacer une image produit un nouvel id, donc une nouvelle URL, que l'ancien filtre téléchargeait déjà. Le vrai cas de péremption vient d'être créé par D3 : les MP3 déjà présents sur les devices y sont en .unknown, et « le fichier est présent » les déclarait à jour. Sans D2, D3 ne réparait que les installations neuves. isResourceOutdated traite donc .unknown comme absent. Deux pièges tranchés en écrivant. La montée v4 laisse les dates existantes à NULL et les considère à jour : backfiller à zéro aurait fait re-télécharger l'intégralité des visites de tous les visiteurs sur leur réseau mobile, au premier lancement. Et la boucle en masse héritée de D1 écrasait la date que la boucle de téléchargement venait d'écrire, DatabaseHelper.insert faisant un UPDATE de la ligne entière quand l'id existe ; pour un téléchargement échoué, c'est l'ancienne date locale qui est conservée, jamais celle du serveur. Co-Authored-By: Claude Opus 5 --- lib/Helpers/DatabaseHelper.dart | 15 +++++-- lib/Models/resourceModel.dart | 11 +++-- lib/Services/downloadConfiguration.dart | 58 ++++++++++++++++++++++--- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/lib/Helpers/DatabaseHelper.dart b/lib/Helpers/DatabaseHelper.dart index c5775b7..71bb397 100644 --- a/lib/Helpers/DatabaseHelper.dart +++ b/lib/Helpers/DatabaseHelper.dart @@ -19,7 +19,7 @@ enum DatabaseTableType { class DatabaseHelper { static const _databaseName = "visit_database.db"; - static const _databaseVersion = 3; + static const _databaseVersion = 4; static const mainTable = 'visitAppContext'; static const columnLanguage = 'language'; @@ -55,6 +55,7 @@ class DatabaseHelper { static const resourcesTable = 'resources'; static const columnSource = 'source'; + static const columnDateUpdate = 'dateUpdate'; static const articleReadTable = 'articleRead'; static const columnLastTimeOpen = 'readTime'; @@ -96,6 +97,12 @@ class DatabaseHelper { await db.execute('ALTER TABLE $configurationsTable ADD COLUMN $columnGridColSpan INT'); await db.execute('ALTER TABLE $configurationsTable ADD COLUMN $columnGridRowSpan INT'); } + if (oldVersion < 4) { + // Les lignes existantes restent à NULL : un fichier déjà téléchargé sans date + // connue est considéré à jour, sinon la montée de version déclencherait un + // re-téléchargement intégral de toutes les visites sur le réseau du visiteur. + await db.execute('ALTER TABLE $resourcesTable ADD COLUMN $columnDateUpdate TEXT'); + } } // SQL code to create the database table @@ -228,7 +235,8 @@ class DatabaseHelper { $columnId TEXT NOT NULL PRIMARY KEY, $columnPath TEXT NOT NULL, $columnSource TEXT NOT NULL, - $columnType INT NOT NULL + $columnType INT NOT NULL, + $columnDateUpdate TEXT ) '''); break; @@ -439,7 +447,8 @@ class DatabaseHelper { id: element["id"], path: element["path"], source: element["source"], - type: ResourceType.values[element["type"]] + type: ResourceType.values[element["type"]], + dateUpdate: element["dateUpdate"] == null ? null : DateTime.tryParse(element["dateUpdate"]) ); } diff --git a/lib/Models/resourceModel.dart b/lib/Models/resourceModel.dart index 24459b2..17d4498 100644 --- a/lib/Models/resourceModel.dart +++ b/lib/Models/resourceModel.dart @@ -7,15 +7,17 @@ class ResourceModel { String? source = ""; String? label = ""; ResourceType? type; + DateTime? dateUpdate; - ResourceModel({this.id, this.path, this.source, this.type}); + ResourceModel({this.id, this.path, this.source, this.type, this.dateUpdate}); Map toMap() { return { 'id': id, 'path': path, 'source': source, - 'type': type?.value + 'type': type?.value, + 'dateUpdate': dateUpdate?.toUtc().toIso8601String() }; } @@ -24,12 +26,13 @@ class ResourceModel { id: json['id'] as String, path: json['path'] as String, source: json['source'] as String, - type: json['type'] as ResourceType + type: json['type'] as ResourceType, + dateUpdate: json['dateUpdate'] == null ? null : DateTime.parse(json['dateUpdate'] as String) ); } @override String toString() { - return 'ResourceModel{id: $id, type: $type, source: $source, path: $path, label: $label}'; + return 'ResourceModel{id: $id, type: $type, source: $source, path: $path, label: $label, dateUpdate: $dateUpdate}'; } } \ No newline at end of file diff --git a/lib/Services/downloadConfiguration.dart b/lib/Services/downloadConfiguration.dart index dec881a..005941b 100644 --- a/lib/Services/downloadConfiguration.dart +++ b/lib/Services/downloadConfiguration.dart @@ -104,7 +104,9 @@ class _DownloadConfigurationWidgetState extends State resource.type != ResourceType.ImageUrl && resource.type != ResourceType.VideoUrl && resource.type != ResourceType.JsonUrl && resource.url != null && !fileList.any((fileL) => fileL.uri.pathSegments.last.contains(resource.id!))); + Map localResourceDates = await readLocalResourceDates(); + + var resourcesToDownload = exportConfigurationDTO.resources!.where((resource) => resource.type != ResourceType.ImageUrl && resource.type != ResourceType.VideoUrl && resource.type != ResourceType.JsonUrl && resource.url != null && isResourceOutdated(resource, fileList, localResourceDates[resource.id])); currentResourceNbr.value = resourcesToDownload.length; @@ -115,9 +117,10 @@ class _DownloadConfigurationWidgetState extends State downloadResource(VisitAppContext visitAppContext, ConfigurationD } } +/// Date de dernière mise à jour connue pour chaque ressource déjà sur le device. +/// Une valeur nulle signifie « fichier téléchargé avant la v4 de la base locale, +/// date inconnue » — pas « périmé ». +Future> readLocalResourceDates() async { + List> rows = await DatabaseHelper.instance.queryAllRows(DatabaseTableType.resources); + return { + for (var row in rows) row["id"] as String: DatabaseHelper.instance.getResourceFromDB(row).dateUpdate + }; +} + +bool isResourceOutdated(ResourceDTO resource, List fileList, DateTime? localDateUpdate) { + var localFile = fileList.where((fileL) => fileL.uri.pathSegments.last.contains(resource.id!)); + + if (localFile.isEmpty) return true; + + // Fichier écrit avec une extension que la table ne savait pas déduire — c'était le + // cas de tous les MP3 avant D3. Il est illisible, et sa date ne le dira jamais : + // le serveur n'a pas changé, c'est le client qui l'avait mal enregistré. + if (localFile.first.uri.pathSegments.last.endsWith(".unknown")) return true; + + if (localDateUpdate == null || resource.dateUpdate == null) return false; + + return resource.dateUpdate!.isAfter(localDateUpdate); +} + String _getExtensionFromContentType(String contentType) { Map contentTypeToExtension = { "image/jpeg": "jpg", "image/jpg": "jpg", "image/png": "png", "image/gif": "gif", + "audio/mpeg": "mp3", "audio/mp3": "mp3", "video/mp4": "mp4", "video/webm": "webm",