Articol tehnic

Salvări sigure la accident HotXLS: fișiere temporare intermediare în Delphi

O salvare care moare la jumătatea drumului, fie dintr-o repornire forțată, un proces omorât, sau un disc care se umple în timpul scrierii, a însemnat tradițional un singur lucru pentru un format construit în jurul scrierilor pe loc: orice octeți au ajuns pe disc înainte de întrerupere sunt ceea ce primiți înapoi, iar un registru de lucru trunchiat nu se mai deschide. HotXLS închide acest mod de eșec printr-o cale de salvare sigură la accident, folosită pentru fiecare fișier XLSX, ODS și XLS clasic pe care îl scrie. Fiecare apel SaveAs scrie fișierul nou complet într-un fișier temporar creat lângă destinație, apoi îl confirmă printr-o singură redenumire atomică MoveFileExW din API-ul Windows, astfel încât o salvare întreruptă poate doar eșua în a produce noul fișier, nu poate niciodată deteriora pe cel pe care îl aveați deja. Aceeași disciplină de intermediere-apoi-schimbare rulează uniform pe ambele motoare de salvare ale HotXLS, scriitorul BIFF8 din spatele XLS clasic și scriitorul OOXML din spatele XLSX și ODS, iar acesta este un tipar care merită împrumutat pentru orice fișier pe care propriul dvs. cod Delphi îl suprascrie direct, fie că sunt foi de calcul sau nu

Ce se întâmplă dacă o salvare de registru de lucru este întreruptă la jumătatea drumului?

Răspunsul direct este că depinde în întregime de modul în care scriitorul atinge fișierul de destinație, iar implementarea comună, deschiderea fișierului țintă și transmiterea conținutului nou direct în el, este în regulă atâta timp cât nimic nu merge niciodată prost. În momentul în care ceva merge prost, un accident, un proces omorât forțat, un share de rețea care se pierde în timpul scrierii, fișierul de pe disc este lăsat în orice stare intermediară ajunsese scriitorul: un director central ZIP care nu a fost niciodată adăugat pentru XLSX sau ODS, sau un flux BIFF căruia îi lipsesc înregistrări pe care un cititor le așteaptă pentru XLS clasic. Excel nu repară asta grațios, și nici niciun alt consumator care așteaptă un fișier complet, așa că rezultatul practic este un registru de lucru care s-a deschis bine ieri și refuză să se deschidă azi

Cum intermediază HotXLS fiecare salvare în spatele unei singure schimbări atomice

HotXLS nu deschide niciodată fișierul de destinație pentru scriere direct, pentru niciunul din cele trei formate pe care le salvează. Secvența are aceeași formă de fiecare dată: construiește ieșirea completă undeva care nu este fișierul pe care utilizatorul îl are deja pe disc, și doar mută-l pe loc odată ce acea construcție a reușit complet. Concret, SaveAs creează un fișier temporar gol în același folder ca și calea țintă, scrie întregul registru de lucru nou în acel fișier temporar, și doar după ce acea scriere revine fără eroare, confirmă fișierul temporar peste destinație printr-o singură redenumire. Nimic din toate acestea nu necesită o proprietate de activat; este pur și simplu ceea ce face SaveAs pentru o cale de fișier simplă, la fiecare apel

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;

Aceeași disciplină se aplică scriitorului XLS clasic, nu doar celui OOXML, iar cele două fișiere temporare împart chiar o convenție de denumire: ambele apelează API-ul Windows GetTempFileNameW cu prefixul hxl, așa că o salvare întreruptă înainte de curățare poate lăsa în urmă un fișier rătăcit cu un nume precum hxl4C2A.tmp așezat lângă registrul dvs. de lucru. Acel fișier nu este corupție, este dovada că mecanismul a funcționat exact așa cum a fost proiectat: scrierea incompletă s-a oprit acolo, iar registrul dvs. de lucru real nu a fost niciodată deschis pentru scriere în primul rând. Văzând unul după un accident este sigur de șters și nimic de investigat

De ce se intermediază fișierul temporar lângă registrul de lucru în loc de în %TEMP%?

Răspunsul scurt este că redenumirea MoveFileExW este atomică doar atunci când sursa și destinația stau pe același volum, iar cel mai sigur mod de a garanta asta fără a cere apelantului să configureze ceva este să deriveze locația fișierului temporar din calea de destinație însăși. HotXLS calculează propriul folder al țintei și predă acel director direct lui GetTempFileNameW, astfel încât fișierul temporar este întotdeauna creat pe același drive, același volum, ca fișierul pe care urmează să îl înlocuiască, automat, pentru fiecare salvare. Dacă biblioteca ar fi intermediat în schimb scrierile în folderul de sistem temp, o cale țintă pe un drive diferit sau un volum de rețea mapat ar transforma pasul final într-o operație cross-volum, pe care API-ul Windows fie o refuză direct, fie, dacă un apelant optează explicit printr-un steag suplimentar pe care HotXLS nu îl setează aici, degradează silențios într-o copiere non-atomică urmată de o ștergere, redeschizând exact fereastra de întrerupere pe care întregul acest mecanism există pentru a o închide

Pasul de confirmare: MoveFileExW, write-through, și ce se întâmplă la eșec

Pasul final al fiecărei salvări este exact un apel API Windows, MoveFileExW, purtând două steaguri care fac fiecare o muncă distinctă. MOVEFILE_REPLACE_EXISTING este ceea ce permite redenumirii să aterizeze pe un fișier care există deja; fără el, o redenumire care țintește o cale existentă pur și simplu eșuează, ceea ce ar înfrânge întregul scop al unei salvări menite să înlocuiască un registru de lucru pe care îl aveți deja. MOVEFILE_WRITE_THROUGH acoperă durabilitatea: îi spune funcției să nu revină până când mutarea nu s-a terminat efectiv pe disc, în loc să revină de îndată ce redenumirea este pur și simplu pusă în coadă, închizând o cursă mai îngustă dar reală, unde un accident imediat după ce SaveAs revine ar putea totuși prinde schimbarea în zbor. Dacă fișierul temporar nu poate fi creat, sau redenumirea finală eșuează din orice motiv (o problemă de permisiune, o destinație blocată, o nepotrivire de volum), HotXLS șterge el însuși fișierul temporar, în loc să lase gunoi în urmă, iar fișierul de destinație este lăsat exact așa cum era înainte de apel

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 însuși păstrează convenția de retur partajată în HotXLS, unu la succes, un număr negativ la eșec, dar un simplu întreg nu spune de ce a eșuat o salvare, iar tratarea fiecărui rezultat negativ la fel aruncă o informație pe care o politică de reîncercare chiar ar putea-o folosi. Proprietatea LastDiagnostic, și colecția Diagnostics mai completă din spatele ei, poartă mesajul pe care HotXLS l-a generat intern, distingând un fișier temporar care nu a putut fi creat de o redenumire pe care Windows a refuzat-o. Un job de lot care înregistrează Code și Message la fiecare SaveAs eșuat construiește exact dovada de care aveți nevoie o dată când un client raportează o salvare care silențios nu a făcut nimic

XLS clasic plătește cu memorie, XLSX și ODS plătesc cu disc

Cele două motoare de salvare ajung la același rezultat sigur la accident prin rute diferite, iar diferența contează dacă deja ajustați unul din ele pentru un job de lot mare. Scriitorul XLS clasic construiește întregul document compus OLE în memorie mai întâi, folosind stocare structurată susținută de un handle de memorie, și doar copiază acel buffer terminat în fișierul temporar frate într-o singură scriere; raționamentul din propriul cod sursă al HotXLS este direct: construirea întregului fișier în memorie mai întâi este ceea ce oprește o salvare eșuată sau anulată să trunchieze vreodată destinația. Scriitorul XLSX și ODS în schimb transmite intrările sale ZIP în fișierul temporar pe măsură ce sunt produse, aceeași intermediere la nivel de fișier cu un profil de memorie diferit. Dacă deja vă bazați pe StreamingWrite pentru a păstra exporturile XLSX mari în interiorul limitei de memorie a unui container, știți că pârghia echivalentă pentru exportul XLS clasic nu există în aceeași formă: garanția sigură la accident este necondiționată în ambele cazuri, dar un export .xls moștenit foarte mare își ține ieșirea completă în RAM indiferent, un compromis acoperit în mai multe detalii în articolul nostru despre scrierile în flux pentru joburile de lot pe server

Aplicarea aceluiași tipar în afara HotXLS, și unde se termină garanția

Împrumutarea tiparului este mai ales o chestiune de a conecta aceleași două apeluri API Windows pe care se bazează HotXLS intern. GetTempFileNameW vă predă un fișier gol, denumit unic, într-un folder pe care îl alegeți, iar MoveFileExW confirmă scrierea dvs. terminată peste destinația reală într-un singur pas; o versiune minimală a aceleiași rutine pe care HotXLS o rulează înainte de fiecare SaveAs arată așa

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;

Garanția are margini reale care merită cunoscute înainte să vă bazați pe ea orbește. Intermedierea unei copii complete înainte de a înlocui originalul înseamnă că o salvare are nevoie temporar de spațiu pe disc pentru atât fișierul vechi, cât și cel nou, aproximativ dublul dimensiunii registrului de lucru pe durata scrierii, ceea ce este în regulă pentru un raport și merită verificat pentru un export de multe gigabytes rulând pe un volum aproape plin. Fișierul temporar trebuie de asemenea să aterizeze în același folder ca destinația, așa că orice cont sub care rulează HotXLS are nevoie de permisiune de creare-fișier pe acel folder specific, nu doar permisiune de a suprascrie singurul fișier despre care știe deja; o implementare care blochează un folder de destinație doar la editări pe loc ale unor nume de fișiere existente specifice, în loc de acces de scriere la nivel de folder, va vedea SaveAs eșuând la pasul fișierului temporar, chiar dacă scrierea directă echivalentă ar fi reușit

Încă două limite merită semnalate clar. O destinație pe un share de rețea sau în interiorul unui folder sincronizat de OneDrive sau un client similar se poate comporta diferit de NTFS local, chiar dacă Windows tot îl raportează ca un singur volum, întrucât driverul de sistem de fișiere din fața lui poate să nu implementeze redenumirea în același mod; dacă ținta dvs. de implementare salvează pe o cale de rețea, merită testată o întrerupere forțată acolo specific, în loc să presupuneți că se transferă comportamentul de disc local. Iar întregul mecanism este limitat la salvarea într-un fișier numit. Apelați SaveAs pe un TStream în schimb, iar HotXLS scrie în orice flux i-ați predat direct, fără fișier de destinație de intermediat sau protejat, pentru că durabilitatea acelui flux (un buffer de memorie, un upload de rețea, un blob de bază de date) este în întregime responsabilitatea codului dvs. din acel punct înainte

O trecere de verificare se poate baza exact pe această garanție ulterior, inclusiv cea încorporată într-un bancă de lucru de audit și conversie a registrelor de lucru: un fișier redeschis care revine scurt sau lipsă este o problemă reală de conversie de urmărit, niciodată o salvare care a fost întreruptă la jumătate și a lăsat ceva ambiguu pe disc. Scrierile intermediate sigure la accident sunt încorporate în SaveAs pentru fiecare registru de lucru XLSX, ODS și XLS clasic produs de componenta HotXLS pentru Delphi și C++Builder, fără nicio configurare necesară pentru a o activa