Article technique

Sauvegardes HotXLS résistantes aux plantages : fichiers temporaires par étapes en Delphi

Une sauvegarde qui meurt à mi-chemin, que ce soit à cause d'un redémarrage forcé, d'un processus tué, ou d'un disque qui se remplit en cours d'écriture, a traditionnellement signifié une chose pour un format construit autour d'écritures sur place : quels que soient les octets ayant atteint le disque avant l'interruption, c'est ce que vous récupérez, et un classeur tronqué ne se rouvre pas. HotXLS ferme ce mode d'échec avec un chemin de sauvegarde résistant aux plantages utilisé pour chaque fichier XLSX, ODS et XLS classique qu'il écrit. Chaque appel à SaveAs écrit le nouveau fichier complet dans un fichier temporaire créé à côté de la destination, puis le valide par un unique renommage atomique MoveFileExW de l'API Windows, si bien qu'une sauvegarde interrompue ne peut qu'échouer à produire le nouveau fichier — elle n'endommage jamais celui que vous aviez déjà. La même discipline de mise en scène puis d'échange s'exécute uniformément sur les deux moteurs de sauvegarde de HotXLS, l'écrivain BIFF8 derrière le XLS classique et l'écrivain OOXML derrière XLSX et ODS, et c'est un schéma qui mérite d'être emprunté pour tout fichier que votre propre code Delphi écrase directement, tableur ou non

Que se passe-t-il si la sauvegarde d'un classeur est interrompue à mi-chemin ?

La réponse directe est que cela dépend entièrement de la manière dont l'écrivain touche le fichier de destination, et l'implémentation courante, ouvrir le fichier cible et diffuser le nouveau contenu directement dedans, fonctionne bien tant que rien ne va jamais mal. Dès que quelque chose se produit, un plantage, un arrêt forcé de processus, un partage réseau qui tombe en cours d'écriture, le fichier sur disque se retrouve dans l'état intermédiaire quel qu'il soit que l'écrivain avait atteint : un répertoire central ZIP jamais ajouté pour XLSX ou ODS, ou un flux BIFF auquel il manque des enregistrements qu'un lecteur attend pour le XLS classique. Excel ne répare pas cela avec élégance, et aucun autre consommateur attendant un fichier complet ne le fait non plus, si bien que le résultat pratique est un classeur qui s'ouvrait bien hier et refuse de s'ouvrir aujourd'hui

Comment HotXLS met en scène chaque sauvegarde derrière un échange atomique

HotXLS n'ouvre jamais le fichier de destination pour écriture directement, pour aucun des trois formats qu'il enregistre. La séquence a toujours la même forme : construire la sortie complète quelque part qui n'est pas le fichier que l'utilisateur a déjà sur disque, et ne le déplacer en place qu'une fois cette construction pleinement réussie. Concrètement, SaveAs crée un fichier temporaire vide dans le même dossier que le chemin cible, écrit tout le nouveau classeur dans ce fichier temporaire, et ce n'est qu'une fois cette écriture revenue sans erreur qu'il valide le fichier temporaire par-dessus la destination avec un unique renommage. Rien de tout cela ne nécessite une propriété à activer ; c'est simplement ce que fait SaveAs pour un chemin de fichier ordinaire, à chaque appel

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Report');
    Sheet.Cells[1, 1].Value := 'Nothing special to enable here';
    // If this call is interrupted, monthly-report.xlsx on disk stays
    // either the old version, complete, or the new version, complete
    if Book.SaveAs('monthly-report.xlsx', xlsxOpenXMLWorkbook) <> 1 then
      raise Exception.Create('Save failed, see Book.LastDiagnostic');
  finally
    Book.Free;
  end;
end;

La même discipline s'applique à l'écrivain XLS classique, pas seulement à celui d'OOXML, et les deux fichiers temporaires partagent même une convention de nommage : les deux appellent l'API Windows GetTempFileNameW avec le préfixe hxl, si bien qu'une sauvegarde interrompue avant le nettoyage peut laisser derrière elle un fichier égaré avec un nom comme hxl4C2A.tmp assis à côté de votre classeur. Ce fichier n'est pas une corruption, c'est la preuve que le mécanisme a fonctionné exactement comme conçu : l'écriture incomplète s'est arrêtée là, et votre véritable classeur n'a jamais été ouvert en écriture en premier lieu. En voir un après un plantage est sans danger à supprimer et n'a rien à investiguer

Pourquoi mettre en scène le fichier temporaire à côté du classeur plutôt que dans %TEMP% ?

La réponse courte est que le renommage de MoveFileExW n'est atomique que lorsque la source et la destination se trouvent sur le même volume, et le moyen le plus sûr de le garantir sans demander à l'appelant de configurer quoi que ce soit est de dériver l'emplacement du fichier temporaire du chemin de destination lui-même. HotXLS calcule le propre dossier de la cible et transmet ce répertoire directement à GetTempFileNameW, si bien que le fichier temporaire est toujours créé sur le même lecteur, le même volume, que le fichier qu'il est sur le point de remplacer, automatiquement, à chaque sauvegarde. Si la bibliothèque avait plutôt mis en scène les écritures dans le dossier temp système, un chemin cible sur un lecteur différent ou un volume réseau mappé transformerait l'étape finale en une opération inter-volume, que l'API Windows soit refuse purement et simplement, soit, si un appelant opte explicitement avec un drapeau supplémentaire que HotXLS ne définit pas ici, dégrade silencieusement en une copie non atomique suivie d'une suppression, rouvrant exactement la fenêtre d'interruption que tout ce mécanisme existe pour fermer

L'étape de validation : MoveFileExW, écriture directe, et ce qui se passe en cas d'échec

L'étape finale de chaque sauvegarde est exactement un unique appel d'API Windows, MoveFileExW, portant deux drapeaux qui effectuent chacun un travail distinct. MOVEFILE_REPLACE_EXISTING est ce qui permet au renommage d'atterrir sur un fichier qui existe déjà ; sans lui, un renommage ciblant un chemin existant échoue simplement, ce qui anéantirait tout l'objectif d'une sauvegarde censée remplacer un classeur que vous avez déjà. MOVEFILE_WRITE_THROUGH couvre la durabilité : il indique à la fonction de ne pas revenir tant que le déplacement n'a pas réellement abouti sur disque, plutôt que de revenir dès que le renommage est simplement mis en file d'attente, fermant une fenêtre de course plus étroite mais réelle où un plantage immédiatement après le retour de SaveAs pourrait encore surprendre l'échange en cours. Si le fichier temporaire ne peut pas être créé, ou si le renommage final échoue pour une raison quelconque (un problème de permission, une destination verrouillée, une incompatibilité de volume), HotXLS supprime lui-même le fichier temporaire plutôt que de laisser des déchets derrière lui, et le fichier de destination reste exactement comme il était avant l'appel

Result := Book.SaveAs(TargetPath, xlsxOpenXMLWorkbook);
if Result <> 1 then
begin
  // TargetPath on disk is unchanged; safe to retry, alert, or
  // fall back to a different path without touching prior output
  LogWriter.Write(Format('SaveAs failed (%d): %s',
    [Book.LastDiagnostic.Code, Book.LastDiagnostic.Message]));
  Exit(False);
end;

SaveAs elle-même conserve la convention de retour partagée dans tout HotXLS, un en cas de succès, un nombre négatif en cas d'échec, mais un simple entier ne dit pas pourquoi une sauvegarde a échoué, et traiter chaque résultat négatif de la même façon jette une information qu'une politique de nouvelle tentative pourrait réellement utiliser. La propriété LastDiagnostic, et la collection Diagnostics plus complète derrière elle, porte le message que HotXLS a généré en interne, distinguant un fichier temporaire qui n'a pas pu être créé d'un renommage que Windows a refusé. Une tâche par lot qui consigne Code et Message à chaque SaveAs échoué constitue exactement les preuves que vous voulez avoir le jour où un client signale une sauvegarde qui n'a silencieusement rien fait

Le XLS classique paie en mémoire, XLSX et ODS paient en disque

Les deux moteurs de sauvegarde atteignent le même résultat résistant aux plantages par des voies différentes, et la différence compte si vous réglez déjà l'un ou l'autre pour une tâche par lot volumineuse. L'écrivain XLS classique construit d'abord tout le document composé OLE en mémoire, en utilisant un stockage structuré adossé à un handle mémoire, et ne copie ce tampon terminé vers le fichier temporaire voisin qu'en une seule écriture ; le raisonnement dans le propre code source de HotXLS est direct : construire tout le fichier en mémoire d'abord est ce qui empêche une sauvegarde échouée ou annulée de jamais tronquer la destination. L'écrivain XLSX et ODS diffuse plutôt ses entrées ZIP dans le fichier temporaire au fur et à mesure qu'elles sont produites, la même mise en scène au niveau du fichier avec un profil mémoire différent. Si vous vous appuyez déjà sur StreamingWrite pour garder les grands exports XLSX dans la limite mémoire d'un conteneur, sachez que le levier équivalent pour l'export XLS classique n'existe pas sous la même forme : la garantie de résistance aux plantages est inconditionnelle dans les deux cas, mais un très grand export .xls hérité garde sa sortie complète en RAM quoi qu'il arrive, un compromis couvert plus en détail dans notre article sur les écritures en flux pour les tâches par lot serveur

Appliquer le même schéma en dehors de HotXLS, et où la garantie s'arrête

Emprunter le schéma est surtout une question de câbler les deux mêmes appels d'API Windows sur lesquels HotXLS s'appuie en interne. GetTempFileNameW vous donne un fichier vide, au nom unique, dans un dossier de votre choix, et MoveFileExW valide votre écriture terminée par-dessus la véritable destination en une seule étape ; une version minimale de la même routine que HotXLS exécute avant chaque SaveAs ressemble à ceci

function SaveFileAtomically(const Path: WideString; const Contents: TBytes): Boolean;
var
  Dir, TempName: WideString;
  Buffer: array[0..MAX_PATH] of WideChar;
  FS: TFileStream;
begin
  Result := False;
  Dir := ExtractFilePath(ExpandFileName(Path));
  FillChar(Buffer, SizeOf(Buffer), 0);
  if GetTempFileNameW(PWideChar(Dir), 'app', 0, @Buffer[0]) = 0 then
    Exit;
  TempName := PWideChar(@Buffer[0]);
  try
    FS := TFileStream.Create(TempName, fmCreate or fmShareExclusive);
    try
      FS.WriteBuffer(Contents[0], Length(Contents));
    finally
      FS.Free;
    end;
    Result := MoveFileExW(PWideChar(TempName), PWideChar(ExpandFileName(Path)),
      MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
  finally
    if not Result then
      DeleteFileW(PWideChar(TempName));
  end;
end;

La garantie a de véritables limites qui méritent d'être connues avant de s'y fier aveuglément. Mettre en scène une copie complète avant de remplacer l'original signifie qu'une sauvegarde a brièvement besoin d'espace disque pour à la fois l'ancien fichier et le nouveau, à peu près le double de la taille du classeur pendant la durée de l'écriture, ce qui est très bien pour un rapport et mérite d'être vérifié pour un export multi-gigaoctets exécuté sur un volume presque plein. Le fichier temporaire doit aussi atterrir dans le même dossier que la destination, si bien que quel que soit le compte sous lequel HotXLS s'exécute, il a besoin d'une permission de création de fichier sur ce dossier spécifiquement, pas simplement d'une permission d'écraser le seul fichier qu'il connaît déjà ; un déploiement qui verrouille un dossier de destination pour n'autoriser que des éditions sur place de noms de fichiers existants spécifiques, plutôt qu'un accès en écriture au niveau du dossier, verra SaveAs échouer à l'étape du fichier temporaire même si l'écriture directe équivalente aurait réussi

Deux autres limites méritent d'être signalées clairement. Une destination sur un partage réseau ou à l'intérieur d'un dossier synchronisé par OneDrive ou un client similaire peut se comporter différemment du NTFS local même si Windows continue de le signaler comme un volume unique, car le pilote de système de fichiers en amont peut ne pas implémenter le renommage de la même façon ; si votre cible de déploiement sauvegarde à travers un chemin réseau, il vaut la peine de tester une interruption forcée là spécifiquement plutôt que de supposer que le comportement du disque local se transpose. Et tout le mécanisme est limité à la sauvegarde dans un fichier nommé. Appelez SaveAs contre un TStream à la place, et HotXLS écrit directement dans le flux que vous lui avez transmis, sans fichier de destination à mettre en scène ou à protéger, car la durabilité de ce flux (un tampon mémoire, un téléversement réseau, un blob de base de données) relève entièrement de la responsabilité de votre code à partir de ce point

Une passe de vérification peut ensuite s'appuyer exactement sur cette garantie, y compris celle intégrée dans un atelier d'audit et de conversion de classeurs : un fichier rouvert qui revient tronqué ou manquant est un véritable problème de conversion à traquer, jamais une sauvegarde interrompue à mi-chemin ayant laissé quelque chose d'ambigu sur disque. Les écritures par étapes résistantes aux plantages sont intégrées à SaveAs pour chaque classeur XLSX, ODS et XLS classique produit par le composant HotXLS pour Delphi et C++Builder, sans configuration requise pour l'activer