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

> Learn how OfficeCLI's Atomic Package Writer guarantees crash-safe document saves with all-or-nothing writes and OS-level atomic replacement. Prevent data corruption.

- Repository: [OfficeAI/OfficeCLI](https://github.com/iofficeai/OfficeCLI)
- Tags: internals
- Published: 2026-08-08

---

**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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Handlers/WordHandler.cs) (lines 45-56), the `AtomicWriteBack` method demonstrates the standard pattern:

```csharp
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:

- [`PowerPointHandler.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/PowerPointHandler.cs) (lines 52-58) — persists `.pptx` files
- [`ExcelHandler.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/ExcelHandler.cs) (lines 459-460) — persists `.xlsx` files

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:

```csharp
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`](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/AtomicPackageWriter.cs) | Core atomic flush implementation with seven-step protocol |
| [`src/officecli/Handlers/WordHandler.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Handlers/WordHandler.cs) | DOCX persistence via `AtomicWriteBack` method |
| [`src/officecli/Handlers/PowerPointHandler.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Handlers/PowerPointHandler.cs) | PPTX persistence |
| [`src/officecli/Handlers/ExcelHandler.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Handlers/ExcelHandler.cs) | XLSX persistence |
| [`src/officecli/CommandBuilder.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/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.