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:
PowerPointHandler.cs(lines 52-58) — persists.pptxfilesExcelHandler.cs(lines 459-460) — persists.xlsxfiles
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.Replacedelivers the atomicity guarantee, withFile.Moveas fallback for new files - All three handlers (
WordHandler,PowerPointHandler,ExcelHandler) use identical delegation patterns for consistent behavior - Optional
postProcessTempenables 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →