D2 : re-télécharger une ressource périmée, base locale v3 vers v4
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 <id>.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 <noreply@anthropic.com>
This commit is contained in:
parent
40c2e618c5
commit
d5f36865b6
@ -19,7 +19,7 @@ enum DatabaseTableType {
|
|||||||
|
|
||||||
class DatabaseHelper {
|
class DatabaseHelper {
|
||||||
static const _databaseName = "visit_database.db";
|
static const _databaseName = "visit_database.db";
|
||||||
static const _databaseVersion = 3;
|
static const _databaseVersion = 4;
|
||||||
|
|
||||||
static const mainTable = 'visitAppContext';
|
static const mainTable = 'visitAppContext';
|
||||||
static const columnLanguage = 'language';
|
static const columnLanguage = 'language';
|
||||||
@ -55,6 +55,7 @@ class DatabaseHelper {
|
|||||||
static const resourcesTable = 'resources';
|
static const resourcesTable = 'resources';
|
||||||
|
|
||||||
static const columnSource = 'source';
|
static const columnSource = 'source';
|
||||||
|
static const columnDateUpdate = 'dateUpdate';
|
||||||
|
|
||||||
static const articleReadTable = 'articleRead';
|
static const articleReadTable = 'articleRead';
|
||||||
static const columnLastTimeOpen = 'readTime';
|
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 $columnGridColSpan INT');
|
||||||
await db.execute('ALTER TABLE $configurationsTable ADD COLUMN $columnGridRowSpan 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
|
// SQL code to create the database table
|
||||||
@ -228,7 +235,8 @@ class DatabaseHelper {
|
|||||||
$columnId TEXT NOT NULL PRIMARY KEY,
|
$columnId TEXT NOT NULL PRIMARY KEY,
|
||||||
$columnPath TEXT NOT NULL,
|
$columnPath TEXT NOT NULL,
|
||||||
$columnSource TEXT NOT NULL,
|
$columnSource TEXT NOT NULL,
|
||||||
$columnType INT NOT NULL
|
$columnType INT NOT NULL,
|
||||||
|
$columnDateUpdate TEXT
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
break;
|
break;
|
||||||
@ -439,7 +447,8 @@ class DatabaseHelper {
|
|||||||
id: element["id"],
|
id: element["id"],
|
||||||
path: element["path"],
|
path: element["path"],
|
||||||
source: element["source"],
|
source: element["source"],
|
||||||
type: ResourceType.values[element["type"]]
|
type: ResourceType.values[element["type"]],
|
||||||
|
dateUpdate: element["dateUpdate"] == null ? null : DateTime.tryParse(element["dateUpdate"])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,15 +7,17 @@ class ResourceModel {
|
|||||||
String? source = "";
|
String? source = "";
|
||||||
String? label = "";
|
String? label = "";
|
||||||
ResourceType? type;
|
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<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
'path': path,
|
'path': path,
|
||||||
'source': source,
|
'source': source,
|
||||||
'type': type?.value
|
'type': type?.value,
|
||||||
|
'dateUpdate': dateUpdate?.toUtc().toIso8601String()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,12 +26,13 @@ class ResourceModel {
|
|||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
path: json['path'] as String,
|
path: json['path'] as String,
|
||||||
source: json['source'] 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
|
@override
|
||||||
String toString() {
|
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}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +104,9 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
print(file.uri.pathSegments.last);
|
print(file.uri.pathSegments.last);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resourcesToDownload = exportConfigurationDTO.resources!.where((resource) => 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<String, DateTime?> 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;
|
currentResourceNbr.value = resourcesToDownload.length;
|
||||||
|
|
||||||
@ -115,9 +117,10 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
if (filePath != null)
|
if (filePath != null)
|
||||||
{
|
{
|
||||||
// Insert in database
|
// Insert in database
|
||||||
ResourceModel resourceModel = ResourceModel(id: resource.id, source: resource.url, path: filePath, type: resource.type);
|
ResourceModel resourceModel = ResourceModel(id: resource.id, source: resource.url, path: filePath, type: resource.type, dateUpdate: resource.dateUpdate);
|
||||||
try {
|
try {
|
||||||
await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap());
|
await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap());
|
||||||
|
localResourceDates[resource.id!] = resource.dateUpdate;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("We got an issue inserting image metadata ${resource.id}");
|
print("We got an issue inserting image metadata ${resource.id}");
|
||||||
}
|
}
|
||||||
@ -136,8 +139,11 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
for (var resource in resourceToDelete) {
|
for (var resource in resourceToDelete) {
|
||||||
print("resource to DELETE");
|
print("resource to DELETE");
|
||||||
print(resource.path);
|
print(resource.path);
|
||||||
// resource.deleteSync();
|
try {
|
||||||
// Preserve call to firebase // TODO uncomment if needed
|
resource.deleteSync();
|
||||||
|
} catch (e) {
|
||||||
|
print("We got an issue deleting local file ${resource.path}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await DatabaseHelper.instance.insert(DatabaseTableType.configurations, ModelsHelper.configurationToMap(widget.configuration));
|
await DatabaseHelper.instance.insert(DatabaseTableType.configurations, ModelsHelper.configurationToMap(widget.configuration));
|
||||||
@ -187,13 +193,21 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
// les ressources via `GetReferencedResourceIds()`, implémentée sur les 13
|
// les ressources via `GetReferencedResourceIds()`, implémentée sur les 13
|
||||||
// sous-types, et les met toutes dans `resources`. On enregistre la charge.
|
// sous-types, et les met toutes dans `resources`. On enregistre la charge.
|
||||||
// Un nouveau type de section est couvert sans toucher à ce fichier.
|
// Un nouveau type de section est couvert sans toucher à ce fichier.
|
||||||
|
//
|
||||||
|
// ⚠️ `insert` fait un UPDATE de toute la ligne quand l'id existe déjà : cette
|
||||||
|
// boucle repasse sur les ressources que la boucle de téléchargement vient
|
||||||
|
// d'enregistrer. Elle doit donc réécrire la date que celle-ci a posée, sinon
|
||||||
|
// elle l'efface et le fichier redevient « sans date connue » à chaque visite.
|
||||||
|
// Et pour une ressource dont le téléchargement a échoué, c'est l'ancienne date
|
||||||
|
// locale qui est conservée — surtout pas celle du serveur, qui la déclarerait
|
||||||
|
// à jour alors que le fichier est absent.
|
||||||
for (var resource in exportConfigurationDTO.resources ?? []) {
|
for (var resource in exportConfigurationDTO.resources ?? []) {
|
||||||
if (resource.id == null) continue;
|
if (resource.id == null) continue;
|
||||||
usedImageOrAudioIds.add(resource.id!);
|
usedImageOrAudioIds.add(resource.id!);
|
||||||
try {
|
try {
|
||||||
await DatabaseHelper.instance.insert(
|
await DatabaseHelper.instance.insert(
|
||||||
DatabaseTableType.resources,
|
DatabaseTableType.resources,
|
||||||
ResourceModel(id: resource.id, source: resource.url, type: resource.type).toMap(),
|
ResourceModel(id: resource.id, source: resource.url, type: resource.type, dateUpdate: localResourceDates[resource.id]).toMap(),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("We got an issue inserting resource ${resource.id}");
|
print("We got an issue inserting resource ${resource.id}");
|
||||||
@ -212,8 +226,12 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO CLEAN AND REMOVE FILES !
|
// `cleanLocalResources` reste désactivée : la table `resources` n'a pas de
|
||||||
//cleanLocalResources(usedImageOrAudioIds, widget.configuration);
|
// colonne `configurationId`, elle est globale à toutes les visites. La purger
|
||||||
|
// avec les ids d'une seule configuration effacerait les lignes des autres.
|
||||||
|
// Les fichiers obsolètes, eux, sont bien supprimés plus haut — leur répertoire
|
||||||
|
// est propre à la configuration. Et rien ne lit la colonne `path` pour le rendu :
|
||||||
|
// `CachedCustomResource` retrouve le fichier en listant le répertoire.
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@ -491,12 +509,38 @@ Future<String?> 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<Map<String, DateTime?>> readLocalResourceDates() async {
|
||||||
|
List<Map<String, dynamic>> 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<FileSystemEntity> 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) {
|
String _getExtensionFromContentType(String contentType) {
|
||||||
Map<String, String> contentTypeToExtension = {
|
Map<String, String> contentTypeToExtension = {
|
||||||
"image/jpeg": "jpg",
|
"image/jpeg": "jpg",
|
||||||
"image/jpg": "jpg",
|
"image/jpg": "jpg",
|
||||||
"image/png": "png",
|
"image/png": "png",
|
||||||
"image/gif": "gif",
|
"image/gif": "gif",
|
||||||
|
"audio/mpeg": "mp3",
|
||||||
"audio/mp3": "mp3",
|
"audio/mp3": "mp3",
|
||||||
"video/mp4": "mp4",
|
"video/mp4": "mp4",
|
||||||
"video/webm": "webm",
|
"video/webm": "webm",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user