Teknisk artikel

HotXLS kraschsäkra sparningar: mellanlagrade temp-filer i Delphi

En sparning som dör halvvägs igenom, oavsett om det beror på en tvingad omstart, en dödad process, eller en disk som blir full mitt i skrivningen, har traditionellt betytt en sak för ett format byggt kring skrivningar på plats: vilka bytes som än nådde disken före avbrottet är vad du får tillbaka, och en avkapad arbetsbok öppnas inte igen. HotXLS stänger den felvägen med en kraschsäker sparväg använd för varje XLSX-, ODS-, och klassisk XLS-fil den skriver. Varje SaveAs-anrop skriver den kompletta nya filen till en temporär fil skapad bredvid destinationen, och committar den sedan med ett enda atomiskt MoveFileExW-namnbyte från Windows-API:et, så en avbruten sparning kan bara misslyckas med att producera den nya filen, den skadar aldrig den du redan hade. Samma mellanlagra-sedan-byt-disciplin körs enhetligt över båda HotXLS:s sparmotorer, BIFF8-skrivaren bakom klassisk XLS och OOXML-skrivaren bakom XLSX och ODS, och det är ett mönster värt att låna för vilken fil din egen Delphi-kod än skriver över direkt, kalkylblad eller ej

Vad händer om en arbetsboks sparning avbryts halvvägs?

Det direkta svaret är att det beror helt på hur skrivaren rör destinationsfilen, och den vanliga implementationen, att öppna målfilen och strömma nytt innehåll direkt in i den, fungerar bra så länge inget någonsin går fel. I samma stund något gör det, en krasch, en tvingad processdödning, en nätverksresurs som tappar anslutningen mitt i skrivningen, lämnas filen på disk i vilket mellantillstånd skrivaren än hade nått: en ZIP-central-katalog som aldrig lades till för XLSX eller ODS, eller en BIFF-ström som saknar poster en läsare förväntar sig för klassisk XLS. Excel reparerar inte det graciöst, och det gör ingen annan konsument som förväntar sig en komplett fil heller, så det praktiska resultatet är en arbetsbok som öppnades bra igår och vägrar öppnas idag

Hur HotXLS mellanlagrar varje sparning bakom en atomisk swap

HotXLS öppnar aldrig destinationsfilen för skrivning direkt, för något av de tre formaten den sparar. Sekvensen har samma form varje gång: bygg den kompletta utdatan någonstans som inte är filen användaren redan har på disk, och flytta den bara på plats när det bygget helt lyckats. Konkret skapar SaveAs en tom temporär fil i samma mapp som målsökvägen, skriver hela den nya arbetsboken in i den temporära filen, och bara efter att den skrivningen returnerar utan fel committar den den temporära filen över destinationen med ett enda namnbyte. Inget av detta kräver en egenskap att aktivera; det är helt enkelt vad SaveAs gör för en vanlig filsökväg, vid varje anrop

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;

Samma disciplin gäller den klassiska XLS-skrivaren, inte bara OOXML-skrivaren, och de två temporära filerna delar till och med en namnkonvention: båda anropar Windows GetTempFileNameW-API:et med hxl-prefixet, så en sparning avbruten före uppstädning kan lämna kvar en vilsekommen fil med ett namn som hxl4C2A.tmp sittande bredvid din arbetsbok. Den filen är inte korruption, den är bevis på att mekanismen fungerade exakt som avsett: den ofullständiga skrivningen stannade där, och din faktiska arbetsbok öppnades aldrig för skrivning från början. Att se en efter en krasch är säkert att radera och inget att undersöka

Varför mellanlagra temp-filen bredvid arbetsboken i stället för i %TEMP%?

Det korta svaret är att MoveFileExW:s namnbyte bara är atomiskt när källan och destinationen sitter på samma volym, och det säkraste sättet att garantera det utan att be anroparen konfigurera något är att härleda den temporära filens plats från destinationssökvägen själv. HotXLS beräknar målets egen mapp och lämnar den mappen direkt till GetTempFileNameW, så den temporära filen skapas alltid på samma enhet, samma volym, som filen den är på väg att ersätta, automatiskt, vid varje sparning. Hade biblioteket i stället mellanlagrat skrivningar i systemets temp-mapp skulle en målsökväg på en annan enhet eller en monterad nätverksvolym göra det sista steget till en volymöverskridande operation, vilket Windows-API:et antingen vägrar rakt av eller, om en anropare uttryckligen väljer in med en extra flagga HotXLS inte sätter här, tyst degraderar till en icke-atomisk kopiering följd av en radering, vilket åter öppnar exakt det avbrottsfönster hela den här mekanismen finns för att stänga

Committsteget: MoveFileExW, write-through, och vad som händer vid misslyckande

Det sista steget i varje sparning är exakt ett Windows-API-anrop, MoveFileExW, som bär två flaggor som var och en gör distinkt arbete. MOVEFILE_REPLACE_EXISTING är vad som tillåter namnbytet att landa på en fil som redan finns; utan den misslyckas ett namnbyte som riktar sig mot en befintlig sökväg helt enkelt, vilket skulle omintetgöra hela poängen med en sparning menad att ersätta en arbetsbok du redan har. MOVEFILE_WRITE_THROUGH täcker varaktighet: den säger åt funktionen att inte returnera förrän flytten faktiskt har slutförts på disk, snarare än att returnera så fort namnbytet bara är köat, vilket stänger ett smalare men verkligt kapplöpningsläge där en krasch omedelbart efter att SaveAs returnerar fortfarande kunde fånga swappen i luften. Om den temporära filen inte kan skapas, eller det slutliga namnbytet misslyckas av någon anledning (ett behörighetsproblem, en låst destination, en volymmismatch), raderar HotXLS den temporära filen själv i stället för att lämna skräp bakom sig, och destinationsfilen lämnas exakt som den var före anropet

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 själv håller den returkonvention som delas genomgående i HotXLS, ett vid lyckat, ett negativt tal vid misslyckande, men ett rått heltal säger inte varför en sparning misslyckades, och att behandla varje negativt resultat på samma sätt kastar bort information en återförsökspolicy faktiskt kunde använda. LastDiagnostic-egenskapen, och den fylligare Diagnostics-samlingen bakom den, bär meddelandet HotXLS genererade internt, som skiljer en temp-fil som inte kunde skapas från ett namnbyte Windows vägrade. Ett batchjobb som loggar Code och Message vid varje misslyckad SaveAs bygger upp exakt de bevis du vill ha den ena gången en kund rapporterar en sparning som tyst inte gjorde något

Klassisk XLS betalar med minne, XLSX och ODS betalar med disk

De två sparmotorerna når samma kraschsäkra utfall via olika vägar, och skillnaden spelar roll om du redan finjusterar någon av dem för ett stort batchjobb. Den klassiska XLS-skrivaren bygger hela OLE-sammansatta dokumentet i minnet först, med strukturerad lagring stödd av ett minneshandtag, och kopierar bara den färdiga bufferten ut till syskon-temp-filen i en enda skrivning; resonemanget i HotXLS:s egen källkod är rakt på sak: att bygga hela filen i minnet först är vad som hindrar en misslyckad eller avbruten sparning från att någonsin avkapa destinationen. XLSX- och ODS-skrivaren strömmar i stället sina ZIP-poster in i den temporära filen allteftersom de produceras, samma fil-nivå-mellanlagring med en annan minnesprofil. Om du redan lutar dig mot StreamingWrite för att hålla stora XLSX-exporter inom en containers minnesgräns, vet att motsvarande spak för klassisk XLS-export inte finns i samma form: den kraschsäkra garantin är ovillkorlig oavsett, men en mycket stor legacy-.xls-export håller sin kompletta utdata i RAM oavsett, en avvägning som täcks djupare i vår artikel om strömmande skrivningar för server-batchjobb

Att applicera samma mönster utanför HotXLS, och var garantin slutar

Att låna mönstret är mest en fråga om att koppla ihop samma två Windows-API-anrop HotXLS förlitar sig på internt. GetTempFileNameW ger dig en unikt namngiven, tom fil i en mapp du väljer, och MoveFileExW committar din färdiga skrivning över den riktiga destinationen i ett steg; en minimal version av samma rutin HotXLS kör innan varje SaveAs ser ut så här

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;

Garantin har verkliga kanter värda att känna till innan du litar blint på den. Att mellanlagra en fullständig kopia innan originalet ersätts betyder att en sparning kortvarigt behöver diskutrymme för både den gamla filen och den nya, ungefär dubbla arbetsbokens storlek under skrivningens varaktighet, vilket är helt okej för en rapport och värt att kontrollera för en flera-gigabyte-export som körs mot en nästan full volym. Den temporära filen måste också landa i samma mapp som destinationen, så vilket konto HotXLS än körs under behöver behörighet att skapa filer i just den mappen, inte bara behörighet att skriva över den ena filen den redan känner till; en driftsättning som låser en destinationsmapp till redigeringar på plats av specifika befintliga filnamn, snarare än mappnivå-skrivåtkomst, kommer att se SaveAs misslyckas vid temp-fil-steget även om den motsvarande direkta skrivningen skulle ha lyckats

Två ytterligare gränser är värda att flagga rakt ut. En destination på en nätverksresurs eller inuti en mapp synkroniserad av OneDrive eller en liknande klient kan bete sig annorlunda än lokal NTFS även om Windows fortfarande rapporterar den som en enda volym, eftersom filsystemsdrivrutinen framför den kanske inte implementerar namnbyte på samma sätt; om ditt driftsättningsmål sparar över en nätverkssökväg är det värt att testa ett tvingat avbrott där specifikt i stället för att anta att lokalt-disk-beteende gäller. Och hela mekanismen är avgränsad till att spara in i en namngiven fil. Anropa SaveAs mot en TStream i stället, och HotXLS skriver in i vilken ström du än gav den direkt, utan någon destinationsfil att mellanlagra eller skydda, eftersom varaktigheten för den strömmen (en minnesbuffert, en nätverksuppladdning, en databasblob) helt är din kods ansvar från den punkten och framåt

En verifieringspassage får förlita sig på exakt den här garantin efteråt, inklusive den sort som byggts in i en arbetsboksrevisions- och konverteringsarbetsbänk: en återöppnad fil som kommer tillbaka kort eller saknas är ett riktigt konverteringsproblem att jaga ner, aldrig en sparning som avbröts halvvägs och lämnade något tvetydigt på disk. Kraschsäkra mellanlagrade skrivningar är inbyggda i SaveAs för varje XLSX-, ODS-, och klassisk XLS-arbetsbok producerad av HotXLS-komponenten för Delphi och C++Builder, utan att någon konfiguration krävs för att slå på det