authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-06-24 10:30:32-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:30-07:00
log9d18e31feef1eeefa766d4a629a527ec3936ace3
treec4e9f12408c048440febf7cc49ccac894bd3c02b
parent25a0648176ce0e0061a035fdf602795368a3b40e

std.fs.AtomicFile: don't forget to flush


2 files changed, 42 insertions(+), 35 deletions(-)

lib/std/fs/AtomicFile.zig+41-33
......@@ -7,8 +7,7 @@ const assert = std.debug.assert;
77const posix = std.posix;
88
99file_writer: File.Writer,
10// TODO either replace this with rand_buf or use []u16 on Windows
11tmp_path_buf: [tmp_path_len:0]u8,
10random_integer: u64,
1211dest_basename: []const u8,
1312file_open: bool,
1413file_exists: bool,
......@@ -17,9 +16,6 @@ dir: Dir,
1716
1817pub const InitError = File.OpenError;
1918
20pub const random_bytes_len = 12;
21const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len);
22
2319/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
2420pub fn init(
2521 dest_basename: []const u8,
......@@ -28,22 +24,16 @@ pub fn init(
2824 close_dir_on_deinit: bool,
2925 write_buffer: []u8,
3026) InitError!AtomicFile {
31 var rand_buf: [random_bytes_len]u8 = undefined;
32 var tmp_path_buf: [tmp_path_len:0]u8 = undefined;
33
3427 while (true) {
35 std.crypto.random.bytes(rand_buf[0..]);
36 const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf);
37 tmp_path_buf[tmp_path.len] = 0;
38
39 const file = dir.createFile(tmp_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
28 const random_integer = std.crypto.random.int(u64);
29 const tmp_sub_path = std.fmt.hex(random_integer);
30 const file = dir.createFile(&tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
4031 error.PathAlreadyExists => continue,
4132 else => |e| return e,
4233 };
43
4434 return .{
4535 .file_writer = file.writer(write_buffer),
46 .tmp_path_buf = tmp_path_buf,
36 .random_integer = random_integer,
4737 .dest_basename = dest_basename,
4838 .file_open = true,
4939 .file_exists = true,
......@@ -54,33 +44,51 @@ pub fn init(
5444}
5545
5646/// Always call deinit, even after a successful finish().
57pub fn deinit(self: *AtomicFile) void {
58 if (self.file_open) {
59 self.file_writer.file.close();
60 self.file_open = false;
47pub fn deinit(af: *AtomicFile) void {
48 if (af.file_open) {
49 af.file_writer.file.close();
50 af.file_open = false;
6151 }
62 if (self.file_exists) {
63 self.dir.deleteFile(&self.tmp_path_buf) catch {};
64 self.file_exists = false;
52 if (af.file_exists) {
53 const tmp_sub_path = std.fmt.hex(af.random_integer);
54 af.dir.deleteFile(&tmp_sub_path) catch {};
55 af.file_exists = false;
6556 }
66 if (self.close_dir_on_deinit) {
67 self.dir.close();
57 if (af.close_dir_on_deinit) {
58 af.dir.close();
6859 }
69 self.* = undefined;
60 af.* = undefined;
61}
62
63pub const FlushError = File.WriteError;
64
65pub fn flush(af: *AtomicFile) FlushError!void {
66 af.file_writer.interface.flush() catch |err| switch (err) {
67 error.WriteFailed => return af.file_writer.err.?,
68 };
7069}
7170
72pub const FinishError = posix.RenameError;
71pub const RenameIntoPlaceError = posix.RenameError;
7372
7473/// On Windows, this function introduces a period of time where some file
7574/// system operations on the destination file will result in
7675/// `error.AccessDenied`, including rename operations (such as the one used in
7776/// this function).
78pub fn finish(self: *AtomicFile) FinishError!void {
79 assert(self.file_exists);
80 if (self.file_open) {
81 self.file_writer.file.close();
82 self.file_open = false;
77pub fn renameIntoPlace(af: *AtomicFile) RenameIntoPlaceError!void {
78 assert(af.file_exists);
79 if (af.file_open) {
80 af.file_writer.file.close();
81 af.file_open = false;
8382 }
84 try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename);
85 self.file_exists = false;
83 const tmp_sub_path = std.fmt.hex(af.random_integer);
84 try posix.renameat(af.dir.fd, &tmp_sub_path, af.dir.fd, af.dest_basename);
85 af.file_exists = false;
86}
87
88pub const FinishError = FlushError || RenameIntoPlaceError;
89
90/// Combination of `flush` followed by `renameIntoPlace`.
91pub fn finish(af: *AtomicFile) FinishError!void {
92 try af.flush();
93 try af.renameIntoPlace();
8694}
lib/std/fs/Dir.zig+1-2
......@@ -2667,12 +2667,11 @@ pub fn copyFile(
26672667 });
26682668 defer atomic_file.deinit();
26692669
2670 const size = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
2670 _ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
26712671 error.ReadFailed => return file_reader.err.?,
26722672 error.WriteFailed => return atomic_file.file_writer.err.?,
26732673 };
26742674 try atomic_file.finish();
2675 _ = size;
26762675}
26772676
26782677pub const AtomicFileOptions = struct {