Ukládání, které umře v polovině — ať už kvůli vynucenému restartu, ukončenému procesu, nebo disku, který se během zápisu zaplní — dřív znamenalo pro formát postavený na zápisu na místě jednu věc: dostanete zpět jen ty bajty, které stihly doputovat na disk před přerušením, a useknutý sešit se znovu neotevře. HotXLS tento druh selhání uzavírá cestou ukládání odolnou proti pádu, použitou pro každý soubor XLSX, ODS a klasický XLS, který zapisuje. Každé volání SaveAs zapíše kompletní nový soubor do dočasného souboru vytvořeného vedle cíle a pak jej potvrdí jediným atomickým přejmenováním MoveFileExW z Windows API, takže přerušené ukládání může nanejvýš selhat v produkci nového souboru — nikdy nepoškodí ten, který jste už měli. Stejná disciplína „nejdřív připrav, pak vyměň" běží jednotně napříč oběma ukládacími enginy HotXLS, zapisovačem BIFF8 za klasickým XLS i zapisovačem OOXML za XLSX a ODS, a je to vzor, který se vyplatí vypůjčit pro jakýkoli soubor, který váš vlastní kód v Delphi přepisuje přímo, ať jde o tabulky, nebo ne
Co se stane, když se ukládání sešitu přeruší v polovině?
Přímá odpověď je, že to úplně závisí na tom, jak se zapisovač dotýká cílového souboru, a běžná implementace, otevření cílového souboru a streamování nového obsahu přímo do něj, je v pořádku, dokud se nic nepokazí. V okamžiku, kdy se něco pokazí — pád, vynucené ukončení procesu, síťové sdílení, které vypadne uprostřed zápisu — soubor na disku zůstane v jakémkoli mezistavu, ke kterému se zapisovač dostal: centrální adresář ZIP, který se u XLSX nebo ODS nikdy nepřipojil, nebo proud BIFF postrádající záznamy, které čtenář u klasického XLS očekává. Excel to elegantně neopraví a stejně tak žádný jiný konzument očekávající kompletní soubor, takže praktickým výsledkem je sešit, který se včera otevřel v pořádku, a dnes se otevřít odmítá
Jak HotXLS připravuje každé uložení za jednou atomickou výměnou
HotXLS nikdy neotevírá cílový soubor přímo pro zápis, u žádného ze tří formátů, které ukládá. Sekvence má vždy stejný tvar: sestavit kompletní výstup někde, co není soubor, který už uživatel má na disku, a přesunout jej na místo teprve poté, co se toto sestavení plně podaří. Konkrétně SaveAs vytvoří prázdný dočasný soubor ve stejné složce jako cílová cesta, zapíše do tohoto dočasného souboru celý nový sešit, a teprve poté, co se tento zápis vrátí bez chyby, potvrdí dočasný soubor přes cíl jediným přejmenováním. Nic z toho nevyžaduje vlastnost k zapnutí; je to prostě to, co SaveAs dělá pro obyčejnou cestu k souboru, při každém volání
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;
Stejná disciplína platí i pro zapisovač klasického XLS, ne jen pro OOXML, a oba dočasné soubory dokonce sdílejí konvenci pojmenování: oba volají Windows API GetTempFileNameW s prefixem hxl, takže ukládání přerušené před úklidem může za sebou zanechat zbloudilý soubor se jménem jako hxl4C2A.tmp sedící vedle vašeho sešitu. Tento soubor není poškození, je to důkaz, že mechanismus fungoval přesně podle návrhu: neúplný zápis se tam zastavil a váš skutečný sešit nebyl vůbec nikdy otevřen pro zápis. Uvidíte-li takový po pádu, je bezpečné jej smazat a není co vyšetřovat
Proč připravovat dočasný soubor vedle sešitu místo v %TEMP%?
Krátká odpověď je, že přejmenování přes MoveFileExW je atomické jen tehdy, když zdroj a cíl sedí na stejném svazku, a nejjistější způsob, jak to zaručit, aniž byste žádali volajícího o jakoukoli konfiguraci, je odvodit umístění dočasného souboru přímo z cílové cesty. HotXLS spočítá vlastní složku cíle a předá tento adresář rovnou GetTempFileNameW, takže se dočasný soubor vždy vytvoří na stejném disku, stejném svazku, jako soubor, který se chystá nahradit, automaticky, při každém uložení. Kdyby knihovna místo toho připravovala zápisy v systémové složce temp, cílová cesta na jiném disku nebo připojeném síťovém svazku by proměnila poslední krok na operaci napříč svazky, kterou Windows API buď rovnou odmítne, nebo, pokud se volající výslovně přihlásí extra příznakem, který zde HotXLS nenastavuje, tiše degraduje na neatomické kopírování následované smazáním, čímž by se znovu otevřelo přesně to okno přerušení, kvůli jehož uzavření tento celý mechanismus existuje
Krok potvrzení: MoveFileExW, zápis naskrz a co se stane při selhání
Poslední krok každého uložení je přesně jedno volání Windows API, MoveFileExW, nesoucí dva příznaky, z nichž každý dělá odlišnou práci. MOVEFILE_REPLACE_EXISTING je to, co umožňuje, aby přejmenování skončilo na souboru, který už existuje; bez něj přejmenování mířící na existující cestu prostě selže, což by zmařilo celý smysl uložení, jehož úkolem je nahradit sešit, který už máte. MOVEFILE_WRITE_THROUGH pokrývá trvanlivost: říká funkci, aby se nevrátila, dokud se přesun skutečně nedokončí na disku, místo aby se vrátila hned, jakmile je přejmenování jen zařazeno do fronty, čímž uzavírá užší, ale skutečnou závodní podmínku, kdy by pád okamžitě po návratu SaveAs mohl přesto zachytit výměnu za letu. Pokud se dočasný soubor nepodaří vytvořit, nebo pokud finální přejmenování selže z jakéhokoli důvodu (problém s oprávněními, uzamčený cíl, nesoulad svazků), HotXLS dočasný soubor sám smaže místo toho, aby po sobě nechal nepořádek, a cílový soubor zůstane přesně takový, jaký byl před voláním
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 samo udržuje konvenci návratové hodnoty sdílenou napříč HotXLS, jedničku při úspěchu, záporné číslo při selhání, ale holé celé číslo neříká, proč se uložení nepodařilo, a zacházet s každým záporným výsledkem stejně zahazuje informaci, kterou by mohla politika opakování skutečně využít. Vlastnost LastDiagnostic, a plnější kolekce Diagnostics za ní, nese zprávu, kterou HotXLS interně vygeneroval, a rozliší dočasný soubor, který se nepodařilo vytvořit, od přejmenování, které Windows odmítl. Dávková úloha, která při každém selhaném SaveAs zaloguje Code a Message, si nashromáždí přesně ten důkaz, který budete chtít tu jednu chvíli, kdy zákazník ohlásí uložení, které tiše nic neudělalo
Klasický XLS platí pamětí, XLSX a ODS platí diskem
Oba ukládací enginy dosahují stejného výsledku odolného proti pádu odlišnou cestou, a rozdíl je důležitý, pokud už doladujete kterýkoli z nich pro velkou dávkovou úlohu. Zapisovač klasického XLS nejdřív sestaví celý složený dokument OLE v paměti, s využitím strukturovaného úložiště podloženého handle paměti, a teprve zkopíruje tento hotový buffer ven do vedlejšího dočasného souboru jedním zápisem; úvaha ve vlastním zdrojovém kódu HotXLS je přímá: sestavit celý soubor nejdřív v paměti je to, co brání selhanému nebo zrušenému uložení, aby kdy useklo cíl. Zapisovač XLSX a ODS místo toho streamuje své záznamy ZIP do dočasného souboru tak, jak vznikají, stejnou přípravu na úrovni souboru s odlišným profilem paměti. Pokud se už opíráte o StreamingWrite, abyste udrželi velké exporty XLSX uvnitř paměťového limitu kontejneru, vězte, že ekvivalentní páčka pro export klasického XLS ve stejné formě neexistuje: záruka odolnosti proti pádu je bezpodmínečná tak jako tak, ale velmi velký export starého .xls drží svůj kompletní výstup v RAM bez ohledu na to, kompromis podrobněji popsaný v našem článku o streamovaném zápisu pro dávkové úlohy na serveru
Aplikace stejného vzoru mimo HotXLS, a kde záruka končí
Vypůjčit si tento vzor je hlavně otázka zapojení stejných dvou volání Windows API, na které se HotXLS interně spoléhá. GetTempFileNameW vám podá jedinečně pojmenovaný, prázdný soubor ve složce podle vašeho výběru, a MoveFileExW jedním krokem potvrdí váš hotový zápis přes skutečný cíl; minimální verze stejné rutiny, kterou HotXLS spouští před každým SaveAs, vypadá takto
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;
Tato záruka má skutečné okraje, které se vyplatí znát dřív, než se na ni slepě spolehnete. Připravit celou kopii před nahrazením originálu znamená, že uložení krátce potřebuje místo na disku pro starý i nový soubor zároveň, zhruba dvojnásobek velikosti sešitu po dobu zápisu, což je v pořádku pro report a stojí za kontrolu u exportu o velikosti více gigabajtů běžícího proti téměř zaplněnému svazku. Dočasný soubor se také musí ocitnout ve stejné složce jako cíl, takže ať už HotXLS běží pod jakýmkoli účtem, potřebuje oprávnění vytvářet soubory přímo v této složce, ne jen oprávnění přepsat ten jeden soubor, o kterém už ví; nasazení, které uzamkne cílovou složku jen na úpravy konkrétních existujících jmen souborů na místě, místo přístupu k zápisu na úrovni složky, uvidí, že SaveAs selže na kroku dočasného souboru, přestože by ekvivalentní přímý zápis uspěl
Dvě další hranice stojí za výslovné zmínění. Cíl na síťovém sdílení nebo uvnitř složky synchronizované OneDrivem nebo podobným klientem se může chovat jinak než místní NTFS, přestože jej Windows pořád hlásí jako jeden svazek, protože ovladač souborového systému před ním nemusí implementovat přejmenování stejným způsobem; pokud vaše nasazení ukládá přes síťovou cestu, vyplatí se tam otestovat vynucené přerušení výslovně, místo aby se předpokládalo, že se chování místního disku přenese beze změny. A celý mechanismus je omezen jen na ukládání do pojmenovaného souboru. Zavolejte SaveAs proti TStream místo toho a HotXLS zapíše přímo do jakéhokoli streamu, který jste mu podali, bez cílového souboru k přípravě nebo ochraně, protože trvanlivost tohoto streamu (paměťový buffer, síťový upload, blob databáze) je od té chvíle úplně odpovědností vašeho kódu
Ověřovací průchod se pak může spolehnout přesně na tuto záruku, včetně toho, který je vestavěný v pracovním nástroji pro audit a konverzi sešitů: znovu otevřený soubor, který se vrátí zkrácený nebo chybějící, je skutečný problém konverze, který je třeba dohledat, nikdy ne uložení, které se přerušilo v polovině a nechalo na disku něco nejednoznačného. Ukládání odolné proti pádu je vestavěné do SaveAs pro každý sešit XLSX, ODS a klasický XLS produkovaný komponentou HotXLS pro Delphi a C++Builder, bez nutnosti jakékoli konfigurace k jeho zapnutí