Documentation
¶
Overview ¶
Package atomicfile writes a file atomically: a reader opening or reading the file sees either the old content or the new content in full, never a part of either.
Contract and Guarantees ¶
Every write follows a strict, bounded sequence:
- Path and parent directory validation: The target path must be in clean canonical form (filepath.Clean(path) == path); non-clean paths (e.g. holding lexical ".." traversals or redundant slashes) are refused immediately to prevent lexical versus physical directory divergence. The parent directory must already exist; atomicfile never creates parent directories silently. The base filename must not exceed 241 bytes so that the temporary file's fixed 14-byte overhead does not exceed the filesystem NAME_MAX (255 bytes). Only standard file permissions 0000-0777 are supported; mode bits outside this mask (such as setuid 04755, setgid, sticky, or file-type bits) are refused. If the target is an existing directory or symlink, or if the parent directory is a symlink, is read-only, does not exist, or fails to resolve via EvalSymlinks, the operation is refused immediately with an error naming the path. A symlink parent is refused before anything is written; the error tells the caller to pass the real directory.
- Exclusive temporary file creation: A temporary file is created exclusively in the SAME physical directory as the target file, using a fixed-length name of the form ".<base>.tmp-%08x" where the suffix is drawn from crypto/rand. This guarantees that concurrent writers never collide and temporary files never escape their parent directory.
- Umask-honoring permissions: The temporary file is created with the caller's requested os.FileMode, allowing the process umask to apply naturally (perm & ^umask), matching the permission semantics of os.OpenFile and os.WriteFile. No explicit chmod is performed by default, so the process umask is preserved.
- Full write and final permissions: The data payload is written to the temporary file in full. With ExactMode, chmod then sets the requested permission bits exactly, overriding the umask before the file is synced.
- Media flush (fsync): The file contents and inode metadata are flushed to durable storage media using fsync (f.Sync()).
- Clean closure: The temporary file descriptor is closed.
- Atomic publication: The temporary file is renamed over the target path (os.Rename). With NoReplace, an exclusive hard link installs it only where the target is absent, and the temporary name is removed.
- Parent directory fsync: The parent directory is fsynced (best-effort) after the publication so that the directory entry is durable on filesystems requiring directory flushes. Errors from directory fsync are ignored on platforms or filesystems where directory fsync is unsupported.
Failure and Cleanup Guarantees ¶
On any failure before publication (creation, write, chmod, sync, close, rename or exclusive link), atomicfile attempts to remove the temporary file immediately (os.Remove), leaving the target file completely untouched. If removing the temporary file fails (for example due to sudden permission loss or filesystem error), the cleanup error is joined to the returned error via errors.Join, explicitly naming the leftover temporary path so callers can detect and inspect any uncollected file. If NoReplace publishes successfully but removal of its temporary name fails, the returned error names both the created target and the leftover name. The complete target remains published; the parent-directory sync is still attempted.
Temporary files left behind by abrupt process termination outside runtime control (such as SIGKILL, power loss, or kernel panic) cannot be swept by defer and are not automatically removed on subsequent invocations.
Directory Boundary and Concurrency Note ¶
The initial symlink and directory check is a safeguard against accidental caller mistakes (e.g. attempting to overwrite a symlink or directory path, or writing through a symlink parent). It is designed for caller-owned directories; it is not an adversarial race-free lock against malicious actors concurrently swapping directory contents on untrusted trees.
Differences from os.WriteFile ¶
Callers migrating from os.WriteFile should note five key differences:
- Atomicity: os.WriteFile truncates and overwrites in-place, allowing concurrent readers to observe truncated or empty intermediate states. atomicfile writes to a sibling temporary file first and renames, ensuring readers always see a complete version.
- Permissions & umask: os.WriteFile preserves the permissions of existing files when overwriting them, and applies the process umask to newly created files. atomicfile creates a new sibling temporary file with perm masked naturally by the process umask (perm & ^umask) and renames it over the target; thus, the resulting file mode reflects perm & ^umask regardless of whether the target already existed. With ExactMode, chmod sets perm exactly before fsync; callers preserving an existing mode must explicitly select that option.
- Inodes and hard links: Because atomicfile replaces the directory entry via rename(2), the target receives a new inode. Existing hard links to the target path continue pointing to the previous inode and will not reflect new writes.
- Special files and FIFOs: If the target is an existing FIFO, socket, or device node, atomicfile does not write into the stream; atomic rename replaces the directory entry with a regular file.
- fsync: atomicfile flushes file data and metadata with fsync before renaming, and performs a best-effort fsync on the parent directory after rename; os.WriteFile performs no fsync.
Power-Loss Durability (What is and is not promised) ¶
What is promised:
- Atomicity for readers: Concurrent readers in this process or other processes will observe either the pre-existing file or the newly written file, never a truncated file, zero-filled pages, or a partially overwritten mixture of the two.
- Data and file-metadata durability: The temporary file data and inode metadata are synced to persistent storage via fsync prior to rename. Once Write returns nil, the file contents are safely flushed to media.
- Directory entry durability: The parent directory is fsynced after rename on a best-effort basis, making directory entry creation durable across sudden power loss on filesystems that support directory fsync.
What is NOT promised:
- Platforms without directory sync: On platforms or filesystems that do not support directory fsync (e.g. Windows or filesystems where syncing a directory descriptor returns an error or is unsupported), parent directory sync is best-effort and silently ignored.
Index ¶
- func Check(path string, perm os.FileMode, opts ...Option) error
- func CheckAfterMkdirAll(path string, perm os.FileMode, opts ...Option) error
- func CheckAppend(path string) error
- func Write(path string, data []byte, perm os.FileMode, opts ...Option) error
- func WriteFile(path string, data []byte, perm os.FileMode, opts ...Option) error
- type Option
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Check ¶
Check makes every check Write makes before it writes, and one more a write learns only by trying: that this process can create a file in the parent. It writes nothing. A plan (a --dry-run) calls it in place of Write, so the plan refuses exactly where the write would.
func CheckAfterMkdirAll ¶
CheckAfterMkdirAll is Check for a write the caller makes after os.MkdirAll(filepath.Dir(path)): a parent that is not there yet is judged as MkdirAll would make it, from the nearest ancestor that is there, which must be a directory this process can create in; a parent that is there is judged by Check.
func CheckAppend ¶
CheckAppend makes the checks an append that creates path when it is absent (os.OpenFile with O_CREATE|O_APPEND, after os.MkdirAll of its parent) needs: the parent as MkdirAll would make it, and a target that is absent or a regular file this process can write.
func Write ¶
Write writes data to path atomically with the specified file mode permissions, subject to the process umask unless ExactMode is selected. If path does not exist, Write creates it; otherwise, Write replaces it atomically. NoReplace instead refuses an existing path and publishes by exclusive hard link.
The write is performed by creating a temporary file in the same directory with mode perm (perm & ^umask), writing the data payload, flushing data and metadata to disk with fsync, closing the file, renaming it over path, and performing a best-effort fsync of the parent directory. On any failure prior to rename, removal of the temporary file is attempted, leaving the target file untouched; if removal fails, the cleanup error is joined to the returned error naming the leftover file.
Permissions note: by default the process umask applies when the temporary file is created. ExactMode explicitly sets perm after writing and before fsync.
Durability note: Write flushes both the file data/metadata (before rename) and the parent directory (after rename, best-effort) to ensure durable directory entry creation across power loss.
Write refuses paths that are not clean (filepath.Clean(path) != path), refuses base names longer than 241 bytes, refuses mode bits outside 0000-0777, refuses to replace directories or symlinks, and returns an error naming path if the parent directory does not exist, is a symlink, is read-only, or fails to resolve. A symlink parent is refused before anything is written; pass the real directory.
Types ¶
type Option ¶
type Option func(*options)
Option configures atomic write behavior.
func ExactMode ¶
func ExactMode() Option
ExactMode configures atomicfile to explicitly chmod the temporary file to perm before the final file sync and rename, even when the process umask would otherwise restrict it.
func NoReplace ¶
func NoReplace() Option
NoReplace installs the complete, synced file only if path is absent, using an exclusive hard link instead of rename. An existing entry, including one created concurrently, is preserved and the error matches os.ErrExist. The filesystem must support hard links. If removing the temporary link after publication fails, the error names it; path already holds the complete data.