Skip to content

fs: add createFileAtomic and AtomicFile to the native API - #592

Merged
lalinsky merged 1 commit into
mainfrom
atomic-file
Jul 19, 2026
Merged

fs: add createFileAtomic and AtomicFile to the native API#592
lalinsky merged 1 commit into
mainfrom
atomic-file

Conversation

@lalinsky

Copy link
Copy Markdown
Owner

Adds a native-API mirror of std.Io.Dir.createFileAtomic / std.Io.File.Atomic, following the same design as zio's existing vtable implementation in io.zig.

API

var af = try dir.createFileAtomic("config.json", .{});
defer af.deinit();

_ = try af.file.write(data, 0);
try af.replace(); // or af.link() to fail with PathAlreadyExists
  • Dir.createFileAtomic(dest_path, options) creates a randomly named exclusive temporary file in the destination's directory (opening the parent dir when dest_path has directory components), so the final move is always a same-directory rename, never a copy. Options: mode (default 0o664) and read.
  • AtomicFile.replace() renames over an existing destination; AtomicFile.link() uses renamePreserve and fails with error.PathAlreadyExists. Unlike std there is no replace flag in the create options: the temp file is always named, so both finish methods are always valid.
  • AtomicFile.deinit() must always be called; it deletes the temp file if it was not moved into place. The delete runs through waitForIoUncancelable, like close(), so a canceled task does not leak temporary files (the std-style deleteFile catch {} would silently skip cleanup under cancellation).
  • Exported as zio.AtomicFile, plus a cwd-level fs.createFileAtomic convenience matching the other module-level helpers.

Testing

Five new unit tests: link to a free destination, link to an occupied destination (verifies PathAlreadyExists, temp cleanup, and that the destination is untouched), replace over an existing file, destination path with directory components, and deinit-without-finish removing the temp file.

Full native suite passes (559/559), and test binaries cross-compile for x86_64-windows-gnu, aarch64-macos and x86_64-freebsd.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 638688b1-d18c-4874-9eb0-4be89dfa5ea0

📥 Commits

Reviewing files that changed from the base of the PR and between 9a769b0 and d6d441e.

📒 Files selected for processing (3)
  • docs/changelog.md
  • src/fs.zig
  • src/zio.zig
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/zio.zig
  • docs/changelog.md
  • src/fs.zig

📝 Walkthrough

Walkthrough

Adds native atomic file creation using temporary files and atomic rename operations. The API supports linking, replacement, and cleanup, is exported through fs and zio, tested across destination and failure cases, and documented in the changelog.

Changes

Atomic file creation

Layer / File(s) Summary
Creation API and initialization
src/fs.zig, src/zio.zig
Adds createFileAtomic, creation options and errors, temporary-file initialization, destination-directory handling, and the public AtomicFile export.
AtomicFile lifecycle
src/fs.zig
Implements exclusive temporary-file creation, atomic link and replace operations, moved-state tracking, and cleanup in deinit.
Lifecycle validation and documentation
src/fs.zig, docs/changelog.md
Tests linking, replacement, collision errors, subdirectory destinations, and abandoned temporary-file cleanup; documents the new APIs and semantics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • lalinsky/zio#382: Related implementation of atomic temporary-file creation and materialization.
  • lalinsky/zio#454: Provides the rename-preserving behavior used by AtomicFile.link().

Poem

A temp file waits in a directory bright,
Then rename makes the final write.
Link may refuse, replace may sing,
deinit sweeps up every dangling thing.
Atomic bytes take flight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: adding createFileAtomic and AtomicFile to the native fs API.
Description check ✅ Passed The description clearly matches the changes and objectives, covering the new atomic file APIs, cleanup behavior, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch atomic-file

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fs.zig`:
- Around line 203-210: Update createFileAtomic to use std.fs.path.dirname and
std.fs.path.basename for splitting dest_path, replacing the unsupported
std.Io.Dir.path references while preserving the existing parent-directory
handling and atomicFileInit flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96a60708-9361-4b65-ae0f-614d8489f0b8

📥 Commits

Reviewing files that changed from the base of the PR and between ec7360f and 9a769b0.

📒 Files selected for processing (3)
  • docs/changelog.md
  • src/fs.zig
  • src/zio.zig

Comment thread src/fs.zig
Comment on lines +203 to +210
pub fn createFileAtomic(self: Dir, dest_path: []const u8, options: CreateFileAtomicOptions) CreateFileAtomicError!AtomicFile {
if (std.Io.Dir.path.dirname(dest_path)) |dirname| {
const parent = try self.openDir(dirname, .{});
errdefer parent.close();
return atomicFileInit(std.Io.Dir.path.basename(dest_path), parent, true, options);
}
return atomicFileInit(dest_path, self, false, options);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the declared/pinned Zig version and check for other usages of this path API.
cat build.zig.zon 2>/dev/null | head -30
rg -n 'std\.Io\.Dir\.path|std\.fs\.path\.(dirname|basename)' --type=zig

Repository: lalinsky/zio

Length of output: 403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- src/fs.zig around the target lines ---\n'
sed -n '1,260p' src/fs.zig | nl -ba | sed -n '170,230p'

printf '\n--- search for Io/path usage in repo ---\n'
rg -n 'std\.Io\.Dir\.path|std\.fs\.path\.(dirname|basename)|std\.Io' src build.zig build.zig.zon examples --type zig || true

Repository: lalinsky/zio

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/fs.zig around lines 170-230 ---'
sed -n '170,230p' src/fs.zig | nl -ba

echo
echo '--- repo search for Io/path usage ---'
rg -n 'std\.Io\.Dir\.path|std\.fs\.path\.(dirname|basename)|std\.Io' src build.zig build.zig.zon examples --type zig || true

Repository: lalinsky/zio

Length of output: 230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/fs.zig around lines 190-215 ---'
awk 'NR>=190 && NR<=215 { printf "%d:%s\n", NR, $0 }' src/fs.zig

echo
echo '--- repo search for path API usage ---'
rg -n 'std\.Io\.Dir\.path|std\.fs\.path\.(dirname|basename)|std\.Io' src build.zig build.zig.zon examples --type zig || true

Repository: lalinsky/zio

Length of output: 11157


🌐 Web query:

Zig 0.16.0 std.Io.Dir.path dirname basename std.fs.path documentation

💡 Result:

In Zig 0.16.0, filesystem and path manipulation APIs are organized within the standard library, though undergoing a transition toward std.Io [1]. 1. std.fs.path: This module remains the primary location for path manipulation functions, including dirname and basename [2][3][4]. These functions operate on path strings (slices of bytes) and are cross-platform aware [3]. - std.fs.path.dirname(path: []const u8)?[]const u8: Returns the parent directory of the given path, or null if no parent exists [3]. - std.fs.path.basename(path: []const u8) []const u8: Returns the final component of the path (e.g., the filename) [3]. 2. std.fs.Dir vs std.Io.Dir: The Zig standard library is actively migrating filesystem APIs from std.fs to std.Io [1]. While std.fs.Dir still exists in 0.16.0, many of its methods are deprecated in favor of their counterparts in std.Io.Dir [5][6]. For example, methods like openFile, openDir, and makeDir on std.fs.Dir serve as wrappers that direct calls to std.Io.Dir [5]. 3. Documentation: The authoritative reference for these APIs in version 0.16.0 is the standard library documentation included with the Zig installation [7]. Because the standard library is evolving, checking the source code of lib/std/fs/path.zig and lib/std/Io/Dir.zig within your specific Zig 0.16.0 distribution is often the most reliable way to see the current signatures and deprecation status [5][2].

Citations:


🌐 Web query:

Zig 0.16.0 "std.Io.Dir.path" dirname basename site:ziglang.org/documentation OR site:github.com/ziglang/zig

💡 Result:

In Zig 0.16.0, std.Io.Dir is a newer interface intended to supersede the traditional std.fs.Dir [1][2]. However, path manipulation functions like dirname and basename are not methods of std.Io.Dir [2]. Instead, path manipulation remains part of the std.fs.path namespace [3][4]. To perform operations such as extracting a directory name or a base name from a string path, you should use the functions provided in std.fs.path: 1. std.fs.path.dirname(path: []const u8)?[]const u8: Returns the directory component of the path, or null if there is no directory component. 2. std.fs.path.basename(path: []const u8) []const u8: Returns the final component of the path. While the Zig standard library has begun migrating filesystem operations to std.Io [1][2], the core logic for parsing and manipulating path strings resides in std.fs.path [3][4]. When working with std.Io.Dir, you use it to interact with the filesystem (e.g., opening, reading, or writing files/directories), and you use std.fs.path to process the path strings themselves [3][4].

Citations:


Switch to std.fs.path for path splitting
std.Io.Dir doesn’t provide path.dirname/basename in Zig 0.16, so src/fs.zig:204-207 should use std.fs.path.dirname(dest_path) and std.fs.path.basename(dest_path) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fs.zig` around lines 203 - 210, Update createFileAtomic to use
std.fs.path.dirname and std.fs.path.basename for splitting dest_path, replacing
the unsupported std.Io.Dir.path references while preserving the existing
parent-directory handling and atomicFileInit flow.

Mirror of std.Io's Dir.createFileAtomic/File.Atomic for zio's native fs
API. The data is written to a randomly named exclusive temporary file in
the destination's directory, then moved into place with an atomic
rename: AtomicFile.replace() overwrites an existing destination,
AtomicFile.link() fails with error.PathAlreadyExists instead.

The temporary file cleanup in AtomicFile.deinit() runs uncancelable,
like close(), so a canceled task does not leak temp files.
@lalinsky
lalinsky merged commit 3aa3423 into main Jul 19, 2026
29 checks passed
@lalinsky
lalinsky deleted the atomic-file branch July 19, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant