about summary refs log tree commit diff
path: root/src/FileSwapper.zig
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2026-06-24 11:57:02 +0900
committerRobert Günzler <r@gnzler.io>2026-09-22 18:01:30 +0200
commitc677e8873e0eae790a1b8b07844a10efa9f63a1e (patch)
tree95bc9709c374d27962a73712d24807a98cc69bb4 /src/FileSwapper.zig
parentfcdf352af595759aa34e6ed037ee10810867a616 (diff)
downloadsite-c677e8873e0eae790a1b8b07844a10efa9f63a1e.tar.gz
wip design for v2
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to 'src/FileSwapper.zig')
-rw-r--r--src/FileSwapper.zig101
1 files changed, 101 insertions, 0 deletions
diff --git a/src/FileSwapper.zig b/src/FileSwapper.zig
new file mode 100644
index 0000000..13bafe4
--- /dev/null
+++ b/src/FileSwapper.zig
@@ -0,0 +1,101 @@
+const std = @import("std");
+
+const FileSwapper = @This();
+const log = std.log.scoped(.FileSwapper);
+
+io: std.Io,
+arena: *std.heap.ArenaAllocator,
+dir: std.Io.Dir,
+target_path: []const u8,
+tmp_path: []const u8,
+tmp: std.Io.File,
+
+pub fn createAt(io: std.Io, allocator: std.mem.Allocator, dir: std.Io.Dir, sub_path: []const u8) !FileSwapper {
+    const arena = try allocator.create(std.heap.ArenaAllocator);
+    errdefer allocator.destroy(arena);
+    arena.* = std.heap.ArenaAllocator.init(allocator);
+
+    const tmp_path = try std.fmt.allocPrint(
+        arena.allocator(),
+        ".{s}",
+        .{sub_path},
+    );
+
+    log.debug("Creating {s}", .{tmp_path});
+
+    const f = try dir.createFile(io, tmp_path, .{});
+
+    return .{
+        .io = io,
+        .arena = arena,
+        .dir = dir,
+        .target_path = try arena.allocator().dupe(u8, sub_path),
+        .tmp_path = tmp_path,
+        .tmp = f,
+    };
+}
+
+fn deinit(self: FileSwapper) void {
+    self.tmp.close(self.io);
+    const allocator = self.arena.child_allocator;
+    self.arena.deinit();
+    allocator.destroy(self.arena);
+}
+
+pub fn file(self: FileSwapper) std.fs.File {
+    return self.tmp;
+}
+
+pub fn writer(self: FileSwapper) std.Io.File.Writer {
+    const buf = self.arena.allocator().alloc(u8, 1024) catch
+        @panic("oom");
+    return self.tmp.writer(self.io, buf);
+}
+
+pub fn swap(self: FileSwapper) !void {
+    log.debug("Swapping {s} -> {s}", .{ self.tmp_path, self.target_path });
+
+    // flush writes
+    try self.tmp.sync(self.io);
+
+    // swap file into final location
+    // NOTE: the tmp fd is still valid after this
+    try std.Io.Dir.rename(
+        self.dir,
+        self.tmp_path,
+        self.dir,
+        self.target_path,
+        self.io,
+    );
+}
+
+pub fn close(self: FileSwapper) void {
+    self.swap() catch
+        @panic("failed to swap target file");
+
+    self.deinit();
+}
+
+test "smoke" {
+    std.testing.log_level = .debug;
+
+    const alloc = std.testing.allocator;
+
+    var tmp = std.testing.tmpDir(.{});
+    defer tmp.cleanup();
+
+    const fileswapper = try FileSwapper.createAt(std.testing.io, alloc, tmp.dir, "test.txt");
+    errdefer fileswapper.deinit();
+
+    var w = fileswapper.writer();
+    try w.interface.writeAll("hi");
+
+    try tmp.dir.access(std.testing.io, fileswapper.tmp_path, .{ .read = true, .write = true });
+
+    try fileswapper.swap();
+
+    const err = tmp.dir.access(std.testing.io, fileswapper.tmp_path, .{ .read = true, .write = true });
+    try std.testing.expectError(error.FileNotFound, err);
+
+    fileswapper.deinit();
+}