Artículo técnico

Guardado a prueba de fallos en HotXLS: archivos temporales por etapas en Delphi

Un guardado que muere a la mitad, ya sea por un reinicio forzado, un proceso terminado, o un disco que se llena a mitad de la escritura, tradicionalmente ha significado una cosa para un formato construido alrededor de escrituras en el mismo lugar: los bytes que llegaron a disco antes de la interrupción son lo que se recupera, y un libro truncado no vuelve a abrir. HotXLS cierra ese modo de fallo con una ruta de guardado a prueba de fallos que se usa para cada archivo XLSX, ODS y XLS clásico que escribe. Cada llamada a SaveAs escribe el nuevo archivo completo en un archivo temporal creado junto al destino, y luego lo confirma con un único renombrado atómico MoveFileExW de la API de Windows, así que un guardado interrumpido solo puede fallar en producir el archivo nuevo, nunca daña el que ya se tenía. La misma disciplina de etapa-y-luego-intercambio se ejecuta de manera uniforme a través de los dos motores de guardado de HotXLS, el escritor BIFF8 detrás del XLS clásico y el escritor OOXML detrás de XLSX y ODS, y es un patrón que vale la pena tomar prestado para cualquier archivo que su propio código Delphi sobrescriba directamente, sean hojas de cálculo o no

¿Qué pasa si se interrumpe a la mitad el guardado de un libro?

La respuesta directa es que depende completamente de cómo el escritor toca el archivo de destino, y la implementación común, abrir el archivo destino y transmitir el contenido nuevo directamente hacia él, funciona bien mientras nada salga mal jamás. En el momento en que algo sale mal, un fallo, un proceso terminado a la fuerza, un recurso compartido de red que se cae a mitad de la escritura, el archivo en disco queda en cualquier estado intermedio al que hubiera llegado el escritor: un directorio central de ZIP que nunca se agregó para XLSX u ODS, o un flujo BIFF al que le faltan registros que un lector espera para el XLS clásico. Excel no repara eso con elegancia, y tampoco lo hace ningún otro consumidor que espere un archivo completo, así que el resultado práctico es un libro que abría bien ayer y se niega a abrir hoy

Cómo HotXLS organiza por etapas cada guardado detrás de un intercambio atómico

HotXLS nunca abre el archivo de destino para escritura directamente, para ninguno de los tres formatos que guarda. La secuencia tiene la misma forma cada vez: construir la salida completa en algún lugar que no sea el archivo que el usuario ya tiene en disco, y solo mover ese resultado a su lugar una vez que esa construcción haya tenido éxito por completo. Concretamente, SaveAs crea un archivo temporal vacío en la misma carpeta que la ruta de destino, escribe el nuevo libro completo en ese archivo temporal, y solo después de que esa escritura retorna sin error confirma el archivo temporal sobre el destino con un único renombrado. Nada de esto requiere activar una propiedad; es simplemente lo que hace SaveAs para una ruta de archivo simple, en cada llamada

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;

La misma disciplina se aplica al escritor XLS clásico, no solo al de OOXML, y los dos archivos temporales incluso comparten una convención de nombres: ambos llaman a la API GetTempFileNameW de Windows con el prefijo hxl, así que un guardado interrumpido antes de la limpieza puede dejar atrás un archivo suelto con un nombre como hxl4C2A.tmp junto a su libro. Ese archivo no es corrupción, es evidencia de que el mecanismo funcionó exactamente como estaba diseñado: la escritura incompleta se detuvo ahí, y su libro real nunca se abrió para escritura en primer lugar. Ver uno después de un fallo es seguro de eliminar y no hay nada que investigar

¿Por qué organizar el archivo temporal junto al libro en lugar de en %TEMP%?

La respuesta corta es que el renombrado de MoveFileExW solo es atómico cuando el origen y el destino están en el mismo volumen, y la forma más segura de garantizar eso sin pedirle a quien llama que configure nada es derivar la ubicación del archivo temporal de la propia ruta de destino. HotXLS calcula la propia carpeta del destino y entrega ese directorio directamente a GetTempFileNameW, así que el archivo temporal siempre se crea en la misma unidad, el mismo volumen, que el archivo que está por reemplazar, automáticamente, en cada guardado. Si la biblioteca en cambio hubiera organizado las escrituras en la carpeta temporal del sistema, una ruta de destino en una unidad distinta o un volumen de red mapeado convertiría el paso final en una operación entre volúmenes, que la API de Windows o bien rechaza directamente, o, si quien llama opta explícitamente con una bandera extra que HotXLS no establece aquí, degrada silenciosamente a una copia no atómica seguida de una eliminación, reabriendo exactamente la ventana de interrupción que todo este mecanismo existe para cerrar

El paso de confirmación: MoveFileExW, escritura directa, y qué pasa si falla

El paso final de cada guardado es exactamente una llamada a la API de Windows, MoveFileExW, que lleva dos banderas que cada una hace un trabajo distinto. MOVEFILE_REPLACE_EXISTING es lo que permite que el renombrado aterrice sobre un archivo que ya existe; sin ella, un renombrado que apunta a una ruta existente simplemente falla, lo que anularía todo el propósito de un guardado destinado a reemplazar un libro que ya se tiene. MOVEFILE_WRITE_THROUGH cubre la durabilidad: le dice a la función que no retorne hasta que el movimiento realmente se haya completado en disco, en lugar de retornar en cuanto el renombrado simplemente queda en cola, cerrando una ventana de carrera más estrecha pero real donde un fallo inmediatamente después de que SaveAs retorna todavía podría atrapar el intercambio en pleno vuelo. Si el archivo temporal no se puede crear, o el renombrado final falla por cualquier razón (un problema de permisos, un destino bloqueado, un desajuste de volumen), HotXLS elimina el archivo temporal él mismo en lugar de dejar basura atrás, y el archivo de destino queda exactamente como estaba antes de la llamada

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 en sí mantiene la convención de retorno compartida en todo HotXLS, uno en caso de éxito, un número negativo en caso de fallo, pero un simple entero no dice por qué falló un guardado, y tratar cada resultado negativo de la misma manera desecha información que una política de reintentos realmente podría usar. La propiedad LastDiagnostic, y la colección Diagnostics más completa detrás de ella, lleva el mensaje que HotXLS generó internamente, distinguiendo un archivo temporal que no se pudo crear de un renombrado que Windows rechazó. Un trabajo por lotes que registra Code y Message en cada SaveAs fallido acumula exactamente la evidencia que se quiere tener la única vez que un cliente reporta un guardado que silenciosamente no hizo nada

El XLS clásico paga con memoria, XLSX y ODS pagan con disco

Los dos motores de guardado llegan al mismo resultado a prueba de fallos por rutas distintas, y la diferencia importa si ya está ajustando alguno de los dos para un trabajo por lotes grande. El escritor XLS clásico construye el documento compuesto OLE completo en memoria primero, usando almacenamiento estructurado respaldado por un handle de memoria, y solo copia ese búfer terminado hacia el archivo temporal hermano en una sola escritura; el razonamiento en el propio código fuente de HotXLS es directo: construir el archivo completo en memoria primero es lo que impide que un guardado fallido o cancelado alguna vez trunque el destino. El escritor de XLSX y ODS, en cambio, transmite sus entradas ZIP hacia el archivo temporal a medida que se producen, la misma organización por etapas a nivel de archivo con un perfil de memoria distinto. Si ya está apoyándose en StreamingWrite para mantener las exportaciones XLSX grandes dentro del límite de memoria de un contenedor, tenga en cuenta que la palanca equivalente para la exportación XLS clásica no existe en la misma forma: la garantía a prueba de fallos es incondicional de cualquier manera, pero una exportación .xls heredada muy grande mantiene su salida completa en RAM sin importar qué, una compensación cubierta con más profundidad en nuestro artículo sobre escrituras en streaming para trabajos por lotes en servidor

Aplicar el mismo patrón fuera de HotXLS, y dónde termina la garantía

Tomar prestado el patrón es sobre todo cuestión de conectar las mismas dos llamadas a la API de Windows en las que se apoya HotXLS internamente. GetTempFileNameW entrega un archivo vacío con nombre único en una carpeta que usted elige, y MoveFileExW confirma su escritura terminada sobre el destino real en un solo paso; una versión mínima de la misma rutina que HotXLS ejecuta antes de cada SaveAs se ve así

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;

La garantía tiene límites reales que vale la pena conocer antes de confiar en ella ciegamente. Organizar por etapas una copia completa antes de reemplazar el original significa que un guardado necesita brevemente espacio en disco tanto para el archivo antiguo como para el nuevo, aproximadamente el doble del tamaño del libro durante la duración de la escritura, lo cual está bien para un informe y vale la pena verificar para una exportación de varios gigabytes que se ejecuta contra un volumen casi lleno. El archivo temporal también tiene que aterrizar en la misma carpeta que el destino, así que cualquier cuenta bajo la que se ejecute HotXLS necesita permiso de creación de archivo en esa carpeta específicamente, no meramente permiso para sobrescribir el único archivo que ya conoce; una implementación que restringe una carpeta de destino a ediciones en el mismo lugar de nombres de archivo existentes específicos, en lugar de acceso de escritura a nivel de carpeta, verá que SaveAs falla en el paso del archivo temporal aunque la escritura directa equivalente hubiera tenido éxito

Dos límites más vale la pena señalar claramente. Un destino en un recurso compartido de red o dentro de una carpeta sincronizada por OneDrive o un cliente similar puede comportarse de manera distinta a NTFS local aunque Windows todavía lo reporte como un solo volumen, ya que el controlador de sistema de archivos frente a él puede no implementar el renombrado de la misma manera; si su implementación de destino guarda a través de una ruta de red, vale la pena probar una interrupción forzada ahí específicamente en lugar de asumir que el comportamiento de disco local se traslada. Y todo el mecanismo está acotado a guardar en un archivo con nombre. Llame a SaveAs contra un TStream en su lugar, y HotXLS escribe en cualquier flujo que le haya entregado directamente, sin ningún archivo de destino que organizar por etapas o proteger, porque la durabilidad de ese flujo (un búfer de memoria, una carga de red, un blob de base de datos) es responsabilidad completa de su código a partir de ese punto

Una pasada de verificación puede apoyarse exactamente en esta garantía después, incluido el tipo integrado en un banco de trabajo de auditoría y conversión de libros: un archivo reabierto que resulta corto o faltante es un problema de conversión real que hay que rastrear, nunca un guardado que se interrumpió a la mitad y dejó algo ambiguo en disco. Las escrituras a prueba de fallos por etapas están integradas en SaveAs para cada libro XLSX, ODS y XLS clásico producido por el componente HotXLS para Delphi y C++Builder, sin necesidad de ninguna configuración para activarlo