Atomic Package Writer in OfficeCLI: How It Guarantees Crash-Safe Document Saves

The AtomicPackageWriter class in OfficeCLI performs all-or-nothing file writes using a temporary file and OS-level atomic replacement, ensuring that power loss, crashes, or killed processes never leave a partially written or corrupted Office document.

OfficeCLI is a cross-platform command-line tool for programmatically editing Microsoft Office documents (DOCX, XLSX, PPTX). When working with Office Open XML (OOXML) packages, the entire document is kept in memory during edits. The atomic package writer serves as the critical safeguard that persists these in-memory changes to disk without risking file corruption.

What Is the Atomic Package Writer?

The AtomicPackageWriter is a static utility class located at src/officecli/Core/AtomicPackageWriter.cs. It exposes a single public method, Flush, which coordinates a seven-step atomic write protocol. This protocol guarantees that the target file is either completely updated to the new version or remains exactly as it was before the operation began—never an inconsistent intermediate state.

The Seven-Step Atomic Write Protocol

Each call to AtomicPackageWriter.Flush executes the following integrity-preserving steps. The source code implements this between lines 7 and 84 of AtomicPackageWriter.cs.

1. Write to a Temporary File

The writer creates a hidden temporary file next to the target using the pattern .{filename}.savetmp-{guid}. The entire in-memory package stream is copied to this temp file using MemoryStream.CopyTo.

This ensures the original document remains untouched throughout the write process. A crash during this phase only leaves a disposable temp file, never a truncated working document.

2. Flush OS Buffers

FileStream.Flush() is invoked on the temporary file stream. This forces the operating system to commit buffered bytes to stable storage before proceeding to the swap operation—protecting against data loss if power fails after the copy completes but before the atomic replacement.

3. Optional Post-Processing

An optional postProcessTemp delegate can rewrite the ZIP contents of the temporary file. Common uses include self-closing tag normalization or other OOXML sanitization. Because this occurs before the final swap, any transformation errors still leave the original file intact.

4. Release the Current Writable Handle

The releaseLock delegate disposes any open stream handles to the target file. On Windows, File.Replace requires that no process holds an open handle to the target; this step satisfies that requirement.

5. Perform the Atomic Swap

The writer uses OS-level atomic primitives:

  • File.Replace(tempPath, targetPath, backupPath: null) — when the target file exists
  • File.Move(tempPath, targetPath) — when the target was removed or is new

File.Replace is guaranteed by the operating system to be atomic: observers always see either the complete old file or the complete new file, never a mixture.

6. Cleanup on Success or Failure

On successful swap, the temporary file is deleted. In any exception path, a catch block removes the temp file. This ensures no orphaned .savetmp-* files accumulate, and failed operations always leave the original document untouched.

7. Re-Open the Caller’s Stream

The reopenLock delegate re-establishes a writable file stream to the (now updated) target file. This allows the editing session to continue even if the swap threw an exception, enabling retry logic at the handler level.

Integration with Document Handlers

All OfficeCLI handlers delegate their persistence logic to AtomicPackageWriter.Flush, ensuring consistent crash-safety across document types.

WordHandler Integration

In src/officecli/Handlers/WordHandler.cs (lines 45-56), the AtomicWriteBack method demonstrates the standard pattern:

private void AtomicWriteBack()
{
    if (_packageStream == null || _backingStream == null) return;

    AtomicPackageWriter.Flush(
        _packageStream,
        _filePath,
        releaseLock: () => { 
            _backingStream!.Dispose(); 
            _backingStream = null; 
        },
        reopenLock: () => {
            _backingStream = new FileStream(_filePath,
                                            FileMode.Open,
                                            FileAccess.ReadWrite,
                                            FileShare.Read);
        });
}

PowerPointHandler and ExcelHandler

The same pattern appears in:

Both handlers pass their respective package streams and file paths, using identical releaseLock/reopenLock delegates to manage their backing streams.

Direct Usage Example

While handlers encapsulate most usage, direct invocation follows this pattern:

using System.IO;
using OfficeCli.Core;

MemoryStream pkg = GetPackageBytes();          // populated OOXML package
string targetPath = @"C:\Docs\Report.docx";

AtomicPackageWriter.Flush(
    pkg,
    targetPath,
    releaseLock: () => {
        fileHandle?.Dispose();
        fileHandle = null;
    },
    reopenLock: () => {
        fileHandle = new FileStream(targetPath,
                                    FileMode.Open,
                                    FileAccess.ReadWrite,
                                    FileShare.Read);
    },
    postProcessTemp: tempPath => {
        // Optional: normalize self-closing XML tags
        ZipUtility.NormalizeSelfClosing(tempPath);
    });

The postProcessTemp parameter is optional; when omitted, the temp file is swapped directly without transformation.

Key Implementation Files

File Responsibility
src/officecli/Core/AtomicPackageWriter.cs Core atomic flush implementation with seven-step protocol
src/officecli/Handlers/WordHandler.cs DOCX persistence via AtomicWriteBack method
src/officecli/Handlers/PowerPointHandler.cs PPTX persistence
src/officecli/Handlers/ExcelHandler.cs XLSX persistence
src/officecli/CommandBuilder.cs Propagates atomic semantics through batch command execution

Summary

  • AtomicPackageWriter provides crash-atomic saves for all OfficeCLI document operations
  • The write-to-temp-then-swap pattern ensures original files are never in a partially written state
  • OS-level File.Replace delivers the atomicity guarantee, with File.Move as fallback for new files
  • All three handlers (WordHandler, PowerPointHandler, ExcelHandler) use identical delegation patterns for consistent behavior
  • Optional postProcessTemp enables safe ZIP transformation without risking the live document

Frequently Asked Questions

What happens if the power fails during an OfficeCLI save?

The temporary file may be incomplete or orphaned, but the original document remains untouched. On next startup, any .savetmp-* files can be safely deleted. The atomic swap only occurs after the temp file is fully written and flushed, so power loss during the swap itself leaves either the old or new complete file—never corruption.

Why does AtomicPackageWriter need releaseLock and reopenLock delegates?

File.Replace on Windows requires that no process hold an open handle to the target file. The releaseLock delegate closes the handler's backing stream to satisfy this requirement. After the swap, reopenLock re-establishes the stream so the editing session can continue. This design keeps the atomic writer ignorant of specific stream management while enabling the necessary synchronization.

Can I use AtomicPackageWriter for non-Office files?

Yes. The class operates on any MemoryStream and file path, with no Office-specific dependencies. The postProcessTemp delegate receives the temporary file path as a string, allowing arbitrary transformations. However, the class is internal to OfficeCLI; direct use requires referencing the assembly or extracting the implementation.

How does this compare to simply using File.WriteAllBytes?

File.WriteAllBytes overwrites the target in-place, leaving a window where the file is truncated or partially written. If the process crashes mid-write, the document is corrupted. AtomicPackageWriter never modifies the target until the complete replacement is ready, providing all-or-nothing semantics that File.WriteAllBytes cannot guarantee.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →