Збереження, що обривається на півдорозі — через примусову перезавантаження, вбитий процес чи диск, що заповнюється посеред запису, — традиційно означало одне для формату, побудованого навколо запису на місці: які байти встигли дійти до диска до переривання, ті й повернуться, а обірвана книга більше не відкриється. HotXLS закриває цей режим збою шляхом стійкого до збоїв збереження, що використовується для кожного файлу XLSX, ODS та класичного XLS, який він записує. Кожен виклик SaveAs записує повний новий файл у тимчасовий файл, створений поряд із призначенням, а потім фіксує його одним атомарним перейменуванням MoveFileExW із Windows API, тож перерване збереження може лише не створити новий файл — воно ніколи не пошкодить той, що вже був. Та сама дисципліна «поетапно, потім підмінити» працює однаково в обох рушіях збереження HotXLS — записувачі BIFF8 за класичним XLS і OOXML за XLSX та ODS, — і це шаблон, вартий запозичення для будь-якого файлу, який ваш власний код Delphi перезаписує напряму, електронні таблиці це чи ні
Що станеться, якщо збереження книги перерветься на півдорозі?
Пряма відповідь у тому, що це повністю залежить від того, як записувач торкається файлу призначення, і поширена реалізація — відкрити цільовий файл і потокувати новий вміст прямо в нього — нормально працює, доки нічого не йде не так. Щойно щось стається — збій, примусове вбивство процесу, мережева папка, що обривається посеред запису, — файл на диску залишається в тому проміжному стані, якого досяг записувач: центральний каталог ZIP, який так і не був доданий для XLSX чи ODS, або потік BIFF, якому бракує записів, яких очікує читач для класичного XLS. Excel не відновлює це гладко, як і жоден інший споживач, що очікує повного файлу, тож практичний результат — книга, що вчора відкривалася нормально, а сьогодні відмовляється відкриватися
Як HotXLS поетапно готує кожне збереження за однією атомарною підміною
HotXLS ніколи не відкриває файл призначення для запису напряму, для жодного з трьох форматів, які зберігає. Послідовність завжди тієї самої форми: побудувати повний вихід десь, що не є файлом, який користувач уже має на диску, і перемістити його на місце лише тоді, коли ця побудова повністю вдалася. Конкретно, SaveAs створює порожній тимчасовий файл у тій самій папці, що й цільовий шлях, записує всю нову книгу в цей тимчасовий файл, і лише після того, як цей запис повертається без помилки, фіксує тимчасовий файл поверх призначення одним перейменуванням. Ніщо з цього не вимагає властивості для увімкнення; це просто те, що SaveAs робить для звичайного шляху файлу, при кожному виклику
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;
Та сама дисципліна застосовується до класичного записувача XLS, а не лише до OOXML, і обидва тимчасові файли навіть поділяють угоду про іменування: обидва викликають API Windows GetTempFileNameW із префіксом hxl, тож збереження, перерване до очищення, може залишити по собі зайвий файл із назвою на кшталт hxl4C2A.tmp поряд із вашою книгою. Цей файл — не пошкодження, це доказ того, що механізм спрацював саме так, як задумано: незавершений запис зупинився там, а вашу справжню книгу взагалі ніколи не відкривали для запису. Побачити такий файл після збою безпечно — його можна видалити, і немає чого розслідувати
Чому тимчасовий файл готується поряд із книгою, а не в %TEMP%?
Коротка відповідь у тому, що перейменування MoveFileExW атомарне лише тоді, коли джерело й призначення розташовані на тому самому томі, і найнадійніший спосіб гарантувати це, не просячи викликача нічого налаштовувати, — вивести розташування тимчасового файлу з самого шляху призначення. HotXLS обчислює власну папку цілі й передає цей каталог прямо в GetTempFileNameW, тож тимчасовий файл завжди створюється на тому самому диску, тому самому томі, що й файл, який він ось-ось замінить, автоматично, при кожному збереженні. Якби бібліотека натомість готувала записи в системній папці temp, шлях цілі на іншому диску чи змонтованому мережевому томі перетворив би останній крок на операцію між томами, яку API Windows або відверто відхиляє, або, якщо викликач явно погоджується додатковим прапорцем, якого HotXLS тут не встановлює, мовчки деградує до неатомарного копіювання з наступним видаленням, знову відкриваючи саме те вікно переривання, для закриття якого й існує весь цей механізм
Крок фіксації: MoveFileExW, наскрізний запис і що відбувається при невдачі
Останній крок кожного збереження — рівно один виклик API Windows, MoveFileExW, що несе два прапорці, кожен із яких виконує окрему роботу. MOVEFILE_REPLACE_EXISTING — це те, що дозволяє перейменуванню приземлитися на файл, який уже існує; без нього перейменування, що цілиться в наявний шлях, просто провалюється, що звело б нанівець увесь сенс збереження, покликаного замінити книгу, яку ви вже маєте. MOVEFILE_WRITE_THROUGH покриває довговічність: він каже функції не повертатися, доки переміщення справді не завершиться на диску, а не повертатися одразу, щойно перейменування лише поставлене в чергу, закриваючи вужчу, але реальну гонитву, де збій одразу після повернення SaveAs усе ще міг би застати підміну в польоті. Якщо тимчасовий файл неможливо створити, або останнє перейменування не вдається з будь-якої причини (проблема з дозволами, заблоковане призначення, невідповідність томів), HotXLS сам видаляє тимчасовий файл замість того, щоб залишати сміття, а файл призначення залишається точно таким, яким був до виклику
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 зберігає конвенцію повернення, спільну для всього HotXLS, — одиниця при успіху, від'ємне число при невдачі, — але голе ціле число не каже, чому збереження провалилося, а трактування кожного від'ємного результату однаково викидає інформацію, яку політика повторних спроб могла б справді використати. Властивість LastDiagnostic, і повніша колекція Diagnostics за нею, несе повідомлення, яке HotXLS згенерував внутрішньо, розрізняючи тимчасовий файл, який не вдалося створити, від перейменування, яке відхилила Windows. Пакетне завдання, що логує Code і Message при кожному невдалому SaveAs, накопичує саме той доказ, який ви захочете мати того одного разу, коли клієнт повідомить про збереження, що мовчки нічого не зробило
Класичний XLS платить пам'яттю, XLSX та ODS платять диском
Два рушії збереження досягають того самого стійкого до збоїв результату різними шляхами, і ця різниця важить, якщо ви вже налаштовуєте будь-який із них для великого пакетного завдання. Записувач класичного XLS спочатку будує весь складений документ OLE в пам'яті, використовуючи структуроване сховище, підкріплене дескриптором пам'яті, і лише копіює цей готовий буфер у сусідній тимчасовий файл одним записом; міркування у власному вихідному коді HotXLS прямі: побудова всього файлу в пам'яті спочатку — це те, що зупиняє невдале чи скасоване збереження від обривання призначення взагалі. Записувач XLSX та ODS натомість потокує свої записи ZIP у тимчасовий файл у міру їх формування — те саме поетапне готування на рівні файлу з іншим профілем пам'яті. Якщо ви вже спираєтеся на StreamingWrite, щоб утримувати великі експорти XLSX у межах пам'яті контейнера, майте на увазі, що еквівалентного важеля для експорту класичного XLS у тій самій формі не існує: гарантія стійкості до збоїв безумовна в обох випадках, але дуже великий застарілий експорт .xls усе одно тримає весь свій вихід в оперативній пам'яті, компроміс, детальніше розглянутий у нашій статті про потокові записи для серверних пакетних завдань
Застосування того самого шаблону поза HotXLS і де закінчується гарантія
Запозичення шаблону — здебільшого питання підключення тих самих двох викликів API Windows, на які HotXLS покладається внутрішньо. GetTempFileNameW дає вам унікально названий, порожній файл у папці на ваш вибір, а MoveFileExW фіксує ваш готовий запис поверх справжнього призначення одним кроком; мінімальна версія тієї самої процедури, яку HotXLS запускає перед кожним SaveAs, виглядає так
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;
У гарантії є реальні межі, про які варто знати, перш ніж покладатися на неї сліпо. Поетапна підготовка повної копії перед заміною оригіналу означає, що збереженню тимчасово потрібне місце на диску як для старого файлу, так і для нового, приблизно вдвічі більше за розмір книги на час запису, що нормально для звіту й варто перевірити для багатогігабайтного експорту, що виконується на майже заповненому томі. Тимчасовий файл також мусить приземлитися в тій самій папці, що й призначення, тож будь-який обліковий запис, під яким працює HotXLS, потребує дозволу на створення файлів саме в цій папці, а не лише дозволу перезаписувати той один файл, про який він уже знає; розгортання, що замикає цільову папку лише на редагування на місці конкретних наявних імен файлів, а не на доступ до запису рівня папки, побачить, що SaveAs провалюється на кроці тимчасового файлу, навіть якщо еквівалентний прямий запис би вдався
Ще дві межі варто прямо позначити. Призначення на мережевій папці чи всередині папки, синхронізованої OneDrive чи подібним клієнтом, може поводитися інакше, ніж локальний NTFS, навіть якщо Windows усе одно повідомляє про нього як про єдиний том, оскільки драйвер файлової системи перед ним може не реалізовувати перейменування так само; якщо ваша ціль розгортання зберігає через мережевий шлях, варто спеціально протестувати там примусове переривання, а не припускати, що поведінка локального диска перенесеться. І весь механізм обмежений збереженням у названий файл. Викличте SaveAs проти TStream натомість, і HotXLS запише в той потік, який ви йому передали, напряму, без файлу призначення для поетапної підготовки чи захисту, бо довговічність цього потоку (буфер пам'яті, мережеве завантаження, blob бази даних) із цього моменту повністю на відповідальності вашого коду
Прохід перевірки згодом може покладатися саме на цю гарантію, включно з тим, що вбудований у верстак аудиту та конвертації книг: перевідкритий файл, що повертається укороченим чи з пропусками, — це справжня проблема конвертації, яку варто з'ясувати, ніколи не збереження, що перервалося на півдорозі й залишило щось неоднозначне на диску. Стійкі до збоїв поетапні записи вбудовані в SaveAs для кожної книги XLSX, ODS та класичного XLS, що виробляється компонентом HotXLS для Delphi та C++Builder, без потреби в жодному налаштуванні для увімкнення