Сохранение, оборвавшееся на середине — из-за принудительной перезагрузки, убитого процесса или диска, заполнившегося посреди записи, — традиционно означало одно для формата, построенного вокруг записи на месте: то, какие байты успели попасть на диск до прерывания, вы и получите обратно, а усечённая книга больше не открывается. 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, и оба временных файла даже разделяют соглашение об именовании: оба вызывают Windows API GetTempFileNameW с префиксом hxl, так что сохранение, прерванное до очистки, может оставить после себя случайный файл с именем вроде hxl4C2A.tmp рядом с вашей книгой. Этот файл — не повреждение, это доказательство, что механизм сработал именно так, как задумано: незавершённая запись остановилась там, а ваша реальная книга вообще ни разу не открывалась для записи. Увидев такой файл после сбоя, его безопасно удалить и не нужно ничего расследовать
Почему временный файл готовится рядом с книгой, а не в %TEMP%?
Короткий ответ — переименование через MoveFileExW атомарно только тогда, когда источник и назначение находятся на одном томе, и самый надёжный способ гарантировать это, не прося вызывающий код что-либо настраивать, — вывести расположение временного файла из самого целевого пути. HotXLS вычисляет собственную папку цели и передаёт этот каталог прямо в GetTempFileNameW, так что временный файл всегда создаётся на том же диске, том же томе, что и файл, который он собирается заменить, автоматически, при каждом сохранении. Если бы библиотека вместо этого готовила записи в системной папке временных файлов, целевой путь на другом диске или подключённом сетевом томе превратил бы финальный шаг в операцию между томами, которую Windows API либо прямо отклоняет, либо, если вызывающий код явно включает дополнительный флаг, который HotXLS здесь не устанавливает, молча деградирует до неатомарного копирования с последующим удалением, вновь открывая как раз то окно прерывания, ради закрытия которого весь этот механизм существует
Шаг фиксации: MoveFileExW, сквозная запись и что происходит при отказе
Финальный шаг каждого сохранения — это ровно один вызов Windows API, 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 и где заканчивается гарантия
Заимствование этого приёма — в основном вопрос подключения тех же двух вызовов Windows API, на которые опирается 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, без какой-либо настройки для включения