En gemning, der dør halvvejs igennem, hvad enten det er fra en tvungen genstart, en dræbt proces, eller en disk der løber fuld midt i skrivningen, har traditionelt betydet én ting for et format bygget omkring på-stedet-skrivninger: hvilke bytes der end nåede disken før afbrydelsen, er det, man får tilbage, og en afkortet projektmappe åbner ikke igen. HotXLS lukker den fejltilstand med en crashsikker gemme-vej, der bruges til hver XLSX-, ODS- og klassisk XLS-fil, den skriver. Hvert SaveAs-kald skriver den komplette nye fil til en midlertidig fil oprettet ved siden af destinationen, og bekræfter den derefter med en enkelt atomar MoveFileExW-omdøbning fra Windows-API'et, så en afbrudt gemning kun kan undlade at producere den nye fil, den beskadiger aldrig den, man allerede havde. Den samme iscenesæt-så-ombyt-disciplin kører ensartet på tværs af HotXLSs to gemme-motorer, BIFF8-skriveren bag klassisk XLS og OOXML-skriveren bag XLSX og ODS, og det er et mønster værd at låne til enhver fil, ens egen Delphi-kode overskriver direkte, regneark eller ej
Hvad sker der, hvis en projektmappe-gemning afbrydes halvvejs igennem?
Det direkte svar er, at det afhænger helt af, hvordan skriveren rører destinationsfilen, og den almindelige implementering, at åbne målfilen og streame nyt indhold direkte ind i den, er fint, så længe intet nogensinde går galt. I det øjeblik noget gør det, et crash, en tvunget proces-drab, en netværksdeling der falder fra midt i skrivningen, efterlades filen på disken i hvilken som helst mellemtilstand skriveren nåede: et ZIP central directory, der aldrig blev tilføjet til XLSX eller ODS, eller en BIFF-stream der mangler poster, en læser forventer for klassisk XLS. Excel reparerer ikke det elegant, og det gør ingen anden forbruger, der forventer en komplet fil, heller ikke, så det praktiske resultat er en projektmappe, der åbnede fint i går og nægter at åbne i dag
Hvordan HotXLS iscenesætter hver gemning bag én atomar ombytning
HotXLS åbner aldrig destinationsfilen til skrivning direkte, for noget af de tre formater, den gemmer. Sekvensen har den samme form hver gang: byg det komplette output et sted, der ikke er den fil, brugeren allerede har på disken, og flyt det først på plads, når den bygning er fuldstændig lykkedes. Konkret opretter SaveAs en tom midlertidig fil i den samme mappe som målstien, skriver hele den nye projektmappe ind i den midlertidige fil, og først efter den skrivning returnerer uden fejl, bekræfter den den midlertidige fil over destinationen med en enkelt omdøbning. Intet af dette kræver en egenskab at fravælge; det er simpelthen, hvad SaveAs gør for en almindelig filsti, ved hvert kald
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;
Den samme disciplin gælder for den klassiske XLS-skriver, ikke kun OOXML-en, og de to midlertidige filer deler endda en navngivningskonvention: begge kalder Windows GetTempFileNameW-API'et med hxl-præfikset, så en gemning afbrudt før oprydning kan efterlade en vildfaren fil med et navn som hxl4C2A.tmp siddende ved siden af projektmappen. Den fil er ikke korruption, den er beviset på, at mekanismen fungerede præcis som designet: den ufuldstændige skrivning stoppede der, og ens egentlige projektmappe blev aldrig åbnet til skrivning i første omgang. At se en efter et crash er sikkert at slette og intet at undersøge
Hvorfor iscenesætte den midlertidige fil ved siden af projektmappen i stedet for i %TEMP%?
Det korte svar er, at MoveFileExW's omdøbning kun er atomar, når kilden og destinationen sidder på samme volumen, og den sikreste måde at garantere det uden at bede kalderen om at konfigurere noget er at udlede den midlertidige fils placering fra selve destinationsstien. HotXLS beregner målets egen mappe og overdrager den mappe direkte til GetTempFileNameW, så den midlertidige fil altid oprettes på samme drev, samme volumen, som den fil, den er ved at erstatte, automatisk, ved hver gemning. Havde biblioteket i stedet iscenesat skrivninger i system-temp-mappen, ville en målsti på et andet drev eller en mappet netværksvolumen have gjort det sidste trin til en operation på tværs af volumener, som Windows-API'et enten afviser direkte eller, hvis en kalder eksplicit fravælger med et ekstra flag HotXLS ikke sætter her, i stilhed nedgraderer til en ikke-atomar kopi efterfulgt af en sletning, og genåbner præcis det afbrydelsesvindue, hele denne mekanisme findes for at lukke
Bekræftelsestrinnet: MoveFileExW, write-through, og hvad der sker ved fejl
Det sidste trin i hver gemning er nøjagtig ét Windows-API-kald, MoveFileExW, der bærer to flag, som hver udfører distinkt arbejde. MOVEFILE_REPLACE_EXISTING er det, der tillader omdøbningen at lande på en fil, der allerede findes; uden det ville en omdøbning, der målretter en eksisterende sti, simpelthen fejle, hvilket ville besejre hele pointen med en gemning ment til at erstatte en projektmappe, man allerede har. MOVEFILE_WRITE_THROUGH dækker holdbarhed: det fortæller funktionen ikke at returnere, før flytningen rent faktisk er fuldført på disken, frem for at returnere, så snart omdøbningen blot er sat i kø, og lukker et snævrere, men reelt kapløb, hvor et crash umiddelbart efter SaveAs returnerer stadig kunne fange ombytningen i gang. Hvis den midlertidige fil ikke kan oprettes, eller den endelige omdøbning fejler af en eller anden grund (et tilladelsesproblem, en låst destination, en volumen-uoverensstemmelse), sletter HotXLS selv den midlertidige fil frem for at efterlade affald, og destinationsfilen efterlades nøjagtig som den var før kaldet
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 selv beholder den delte returkonvention på tværs af HotXLS, ét ved succes, et negativt tal ved fejl, men et rent heltal siger ikke hvorfor en gemning fejlede, og at behandle hvert negativt resultat på samme måde smider information væk, en genforsøgs-politik rent faktisk kunne bruge. LastDiagnostic-egenskaben, og den fuldere Diagnostics-samling bag den, bærer den besked HotXLS genererede internt, og skelner en midlertidig fil, der ikke kunne oprettes, fra en omdøbning, Windows afviste. Et batch-job, der logger Code og Message ved hver mislykket SaveAs, opbygger præcis det bevis, man vil have den ene gang, en kunde rapporterer en gemning, der i stilhed ikke gjorde noget
Klassisk XLS betaler med hukommelse, XLSX og ODS betaler med disk
De to gemme-motorer når det samme crashsikre resultat via forskellige veje, og forskellen betyder noget, hvis man allerede finjusterer den ene til et stort batch-job. Den klassiske XLS-skriver bygger hele OLE-compound-dokumentet i hukommelsen først, ved brug af struktureret lagring bakket op af et hukommelseshandle, og kopierer først den færdige buffer ud til den søster-midlertidige fil i én skrivning; ræsonnementet i HotXLSs egen kildekode er direkte: at bygge hele filen i hukommelsen først er det, der stopper en mislykket eller annulleret gemning fra nogensinde at afkorte destinationen. XLSX- og ODS-skriveren streamer i stedet sine ZIP-poster ind i den midlertidige fil, efterhånden som de produceres, den samme fil-niveau-iscenesættelse med en anden hukommelsesprofil. Hvis man allerede læner sig på StreamingWrite for at holde store XLSX-eksporter inden for en containers hukommelsesgrænse, ved at det tilsvarende håndtag til klassisk XLS-eksport ikke findes i samme form: den crashsikre garanti er ubetinget enten som, men en meget stor legacy-.xls-eksport holder sit komplette output i RAM uanset, en afvejning dækket dybere i vores artikel om streaming-skrivninger til server-batch-jobs
At anvende det samme mønster uden for HotXLS, og hvor garantien slutter
At låne mønstret er mest et spørgsmål om at koble de samme to Windows-API-kald sammen, HotXLS er afhængig af internt. GetTempFileNameW overdrager dig en unikt navngivet, tom fil i en mappe, man vælger, og MoveFileExW bekræfter ens færdige skrivning over den rigtige destination i ét trin; en minimal version af den samme rutine HotXLS kører før hver SaveAs ser sådan ud
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;
Garantien har reelle grænser, der er værd at kende, før man stoler blindt på den. At iscenesætte en fuld kopi før man erstatter originalen betyder, at en gemning kortvarigt har brug for diskplads til både den gamle fil og den nye, groft sagt det dobbelte af projektmappens størrelse for skrivningens varighed, hvilket er fint for en rapport og værd at tjekke for en flere-gigabyte-eksport, der kører mod en næsten fuld volumen. Den midlertidige fil skal også lande i samme mappe som destinationen, så uanset hvilken konto HotXLS kører under, har brug for opret-fil-tilladelse på netop den mappe, ikke blot tilladelse til at overskrive den ene fil, den allerede kender til; en udrulning der låser en destinationsmappe ned til på-stedet-redigeringer af specifikke eksisterende filnavne, frem for mappe-niveau-skriveadgang, vil se SaveAs fejle på det midlertidige-fil-trin, selvom den tilsvarende direkte skrivning ville være lykkedes
To flere grænser er værd at flage klart. En destination på en netværksdeling eller inde i en mappe synkroniseret af OneDrive eller en lignende klient kan opføre sig forskelligt fra lokal NTFS, selvom Windows stadig rapporterer den som en enkelt volumen, da filsystem-driveren foran den måske ikke implementerer omdøbning på samme måde; hvis ens udrulningsmål gemmer på tværs af en netværkssti, er det værd at teste en tvungen afbrydelse der specifikt frem for at antage, lokal-disk-opførsel overføres. Og hele mekanismen er afgrænset til at gemme ind i en navngivet fil. Kald SaveAs mod en TStream i stedet, og HotXLS skriver ind i hvilken som helst stream, man overdrog den, direkte, uden nogen destinationsfil at iscenesætte eller beskytte, fordi holdbarheden af den stream (en hukommelsesbuffer, en netværksupload, en database-blob) er helt ens kodes ansvar fra det punkt og fremad
En verificeringsgennemgang får lov at stole på præcis den garanti bagefter, inklusive den slags indbygget i en workbench til projektmappe-audit og -konvertering: en genåbnet fil, der kommer tilbage kort eller manglende, er et rigtigt konverteringsproblem at spore, aldrig en gemning der blev afbrudt halvvejs og efterlod noget tvetydigt på disken. Crashsikre iscenesatte skrivninger er indbygget i SaveAs for hver XLSX-, ODS- og klassisk XLS-projektmappe produceret af HotXLS-komponenten til Delphi og C++Builder, uden nogen konfiguration påkrævet for at slå den til