Uma gravação que morre a meio, seja por um reinício forçado, um processo terminado, ou um disco que enche a meio da escrita, tradicionalmente significava uma coisa para um formato construído à volta de escritas no próprio local: quaisquer bytes que tenham chegado ao disco antes da interrupção são o que se recupera, e um livro de cálculo truncado não volta a abrir. O HotXLS fecha esse modo de falha com um caminho de gravação seguro contra falhas, usado em cada ficheiro XLSX, ODS, e XLS clássico que escreve. Cada chamada a SaveAs escreve o ficheiro novo completo num ficheiro temporário criado ao lado do destino, e só depois o confirma com uma única renomeação atómica MoveFileExW da API do Windows, pelo que uma gravação interrompida só pode falhar em produzir o ficheiro novo, nunca danifica o que já se tinha. A mesma disciplina de faseamento-depois-troca corre uniformemente através dos dois motores de gravação do HotXLS, o escritor BIFF8 por trás do XLS clássico e o escritor OOXML por trás do XLSX e do ODS, e é um padrão que vale a pena tomar de empréstimo para qualquer ficheiro que o seu próprio código Delphi sobrescreva diretamente, folhas de cálculo ou não
O que acontece se a gravação de um livro de cálculo for interrompida a meio?
A resposta direta é que depende inteiramente de como o escritor toca no ficheiro de destino, e a implementação comum, abrir o ficheiro alvo e transmitir novo conteúdo diretamente para ele, é aceitável enquanto nada correr mal. No momento em que algo corre, uma falha, um processo terminado à força, uma partilha de rede que cai a meio da escrita, o ficheiro em disco fica no estado intermédio em que quer que o escritor tivesse chegado: um diretório central ZIP que nunca chegou a ser anexado para XLSX ou ODS, ou um fluxo BIFF sem os registos que um leitor espera para o XLS clássico. O Excel não repara isso graciosamente, nem qualquer outro consumidor que espere um ficheiro completo, pelo que o resultado prático é um livro de cálculo que abria bem ontem e recusa abrir hoje
Como faseia o HotXLS cada gravação por trás de uma única troca atómica
O HotXLS nunca abre o ficheiro de destino para escrita diretamente, para nenhum dos três formatos que grava. A sequência tem sempre a mesma forma: construir a saída completa algures que não seja o ficheiro que o utilizador já tem em disco, e só o mover para o lugar assim que essa construção tiver sido concluída com sucesso. Concretamente, SaveAs cria um ficheiro temporário vazio na mesma pasta que o caminho alvo, escreve todo o novo livro de cálculo nesse ficheiro temporário, e só depois de essa escrita regressar sem erro é que confirma o ficheiro temporário sobre o destino com uma única renomeação. Nada disto exige uma propriedade a ativar; é simplesmente o que SaveAs faz para um caminho de ficheiro simples, em cada chamada
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;
A mesma disciplina aplica-se ao escritor XLS clássico, não apenas ao OOXML, e os dois ficheiros temporários até partilham uma convenção de nomenclatura: ambos chamam a API GetTempFileNameW do Windows com o prefixo hxl, pelo que uma gravação interrompida antes da limpeza pode deixar para trás um ficheiro perdido com um nome como hxl4C2A.tmp ao lado do seu livro de cálculo. Esse ficheiro não é corrupção, é prova de que o mecanismo funcionou exatamente como projetado: a escrita incompleta parou ali, e o seu livro de cálculo real nunca chegou a ser aberto para escrita, em primeiro lugar. Ver um destes depois de uma falha é seguro apagar e nada a investigar
Porque faseia o ficheiro temporário ao lado do livro de cálculo em vez de em %TEMP%?
A resposta curta é que a renomeação do MoveFileExW só é atómica quando a origem e o destino estão no mesmo volume, e a forma mais segura de garantir isso sem pedir ao chamador para configurar seja o que for é derivar a localização do ficheiro temporário do próprio caminho de destino. O HotXLS calcula a própria pasta do alvo e entrega esse diretório diretamente ao GetTempFileNameW, pelo que o ficheiro temporário é sempre criado na mesma unidade, o mesmo volume, que o ficheiro que está prestes a substituir, automaticamente, em cada gravação. Se a biblioteca tivesse antes faseado as escritas na pasta temporária do sistema, um caminho de destino numa unidade diferente ou num volume de rede mapeado transformaria o passo final numa operação entre volumes, que a API do Windows recusa por completo ou, se um chamador optar explicitamente por uma flag adicional que o HotXLS não define aqui, degrada silenciosamente numa cópia não atómica seguida de uma eliminação, reabrindo exatamente a janela de interrupção que todo este mecanismo existe para fechar
O passo de confirmação: MoveFileExW, write-through, e o que acontece em caso de falha
O passo final de cada gravação é exatamente uma chamada à API do Windows, MoveFileExW, transportando duas flags que fazem cada uma um trabalho distinto. MOVEFILE_REPLACE_EXISTING é o que permite que a renomeação recaia sobre um ficheiro já existente; sem ela, uma renomeação que aponte para um caminho existente simplesmente falha, o que anularia todo o propósito de uma gravação destinada a substituir um livro de cálculo que já se tem. MOVEFILE_WRITE_THROUGH cobre a durabilidade: diz à função para não regressar até a mudança ter efetivamente sido concluída em disco, em vez de regressar assim que a renomeação está meramente em fila, fechando uma janela de corrida mais estreita mas real, em que uma falha imediatamente após SaveAs regressar ainda poderia apanhar a troca em curso. Se o ficheiro temporário não puder ser criado, ou a renomeação final falhar por qualquer razão (um problema de permissão, um destino bloqueado, uma incompatibilidade de volume), o HotXLS apaga o próprio ficheiro temporário em vez de deixar lixo para trás, e o ficheiro de destino fica exatamente como estava antes da chamada
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;
O próprio SaveAs mantém a convenção de retorno partilhada por todo o HotXLS, um em caso de sucesso, um número negativo em caso de falha, mas um simples inteiro não diz porque uma gravação falhou, e tratar cada resultado negativo da mesma forma deita fora informação que uma política de repetição poderia efetivamente usar. A propriedade LastDiagnostic, e a coleção Diagnostics mais completa por trás dela, transporta a mensagem que o HotXLS gerou internamente, distinguindo um ficheiro temporário que não pôde ser criado de uma renomeação que o Windows recusou. Um trabalho em lote que regista Code e Message em cada SaveAs falhado acumula exatamente a evidência que se quer ter na única vez em que um cliente reporta uma gravação que silenciosamente não fez nada
O XLS clássico paga com memória, o XLSX e o ODS pagam com disco
Os dois motores de gravação chegam ao mesmo resultado seguro contra falhas por caminhos diferentes, e a diferença importa se já estiver a otimizar um deles para um grande trabalho em lote. O escritor XLS clássico constrói todo o documento composto OLE em memória primeiro, usando armazenamento estruturado suportado por um handle de memória, e só copia esse buffer concluído para o ficheiro temporário irmão numa única escrita; o raciocínio no próprio código-fonte do HotXLS é direto: construir o ficheiro inteiro em memória primeiro é o que impede uma gravação falhada ou cancelada de alguma vez truncar o destino. O escritor XLSX e ODS, pelo contrário, transmite as suas entradas ZIP para o ficheiro temporário à medida que são produzidas, o mesmo faseamento ao nível de ficheiro com um perfil de memória diferente. Se já estiver a apoiar-se em StreamingWrite para manter grandes exportações XLSX dentro do limite de memória de um contentor, saiba que a alavanca equivalente para a exportação XLS clássica não existe na mesma forma: a garantia segura contra falhas é incondicional em ambos os casos, mas uma exportação .xls legada muito grande mantém a sua saída completa em RAM de qualquer forma, uma compensação abordada com mais profundidade no nosso artigo sobre escritas em fluxo para trabalhos em lote de servidor
Aplicar o mesmo padrão fora do HotXLS, e onde a garantia termina
Tomar o padrão de empréstimo é sobretudo uma questão de ligar as mesmas duas chamadas à API do Windows em que o HotXLS se apoia internamente. GetTempFileNameW entrega um ficheiro vazio, com nome único, numa pasta à sua escolha, e MoveFileExW confirma a sua escrita concluída sobre o destino real num único passo; uma versão mínima da mesma rotina que o HotXLS executa antes de cada SaveAs tem este aspeto
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;
A garantia tem limites reais que vale a pena conhecer antes de confiar nela cegamente. Faseamento de uma cópia completa antes de substituir o original significa que uma gravação precisa brevemente de espaço em disco tanto para o ficheiro antigo como para o novo, aproximadamente o dobro do tamanho do livro de cálculo durante a escrita, o que é aceitável para um relatório e vale a pena verificar para uma exportação de vários gigabytes a correr contra um volume quase cheio. O ficheiro temporário também tem de cair na mesma pasta que o destino, pelo que a conta sob a qual o HotXLS está a correr precisa de permissão de criação de ficheiro nessa pasta especificamente, não apenas permissão para sobrescrever o único ficheiro que já conhece; uma implantação que bloqueie uma pasta de destino apenas a edições no próprio local de nomes de ficheiro específicos existentes, em vez de acesso de escrita ao nível da pasta, verá SaveAs falhar no passo do ficheiro temporário, mesmo que a escrita direta equivalente tivesse tido sucesso
Vale a pena assinalar claramente mais duas fronteiras. Um destino numa partilha de rede ou dentro de uma pasta sincronizada pelo OneDrive ou por um cliente semelhante pode comportar-se de forma diferente do NTFS local, mesmo que o Windows continue a reportá-lo como um único volume, uma vez que o controlador de sistema de ficheiros à sua frente pode não implementar a renomeação da mesma forma; se o seu alvo de implantação gravar através de um caminho de rede, vale a pena testar uma interrupção forçada especificamente ali, em vez de assumir que o comportamento do disco local se transporta. E todo o mecanismo está limitado a gravar num ficheiro nomeado. Chame SaveAs contra um TStream em vez disso, e o HotXLS escreve diretamente no fluxo que lhe entregou, sem qualquer ficheiro de destino a fasear ou proteger, porque a durabilidade desse fluxo (um buffer de memória, um envio de rede, um blob de base de dados) é inteiramente responsabilidade do seu código a partir desse ponto
Uma passagem de verificação pode depois apoiar-se exatamente nesta garantia, incluindo a incorporada num banco de trabalho de auditoria e conversão de livros de cálculo: um ficheiro reaberto que regresse incompleto ou ausente é um verdadeiro problema de conversão a investigar, nunca uma gravação que foi interrompida a meio e deixou algo ambíguo em disco. As escritas faseadas seguras contra falhas estão incorporadas em SaveAs para cada livro de cálculo XLSX, ODS, e XLS clássico produzido pelo componente HotXLS para Delphi e C++Builder, sem qualquer configuração necessária para a ativar