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

This commit is contained in:
Andrew Kelley 2025-06-24 10:30:32 -07:00
parent 25a0648176
commit 9d18e31fee
2 changed files with 42 additions and 35 deletions

View File

@ -7,8 +7,7 @@ const assert = std.debug.assert;
const posix = std.posix;
file_writer: File.Writer,
// TODO either replace this with rand_buf or use []u16 on Windows
tmp_path_buf: [tmp_path_len:0]u8,
random_integer: u64,
dest_basename: []const u8,
file_open: bool,
file_exists: bool,
@ -17,9 +16,6 @@ dir: Dir,
pub const InitError = File.OpenError;
pub const random_bytes_len = 12;
const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len);
/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
pub fn init(
dest_basename: []const u8,
@ -28,22 +24,16 @@ pub fn init(
close_dir_on_deinit: bool,
write_buffer: []u8,
) InitError!AtomicFile {
var rand_buf: [random_bytes_len]u8 = undefined;
var tmp_path_buf: [tmp_path_len:0]u8 = undefined;
while (true) {
std.crypto.random.bytes(rand_buf[0..]);
const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf);
tmp_path_buf[tmp_path.len] = 0;
const file = dir.createFile(tmp_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
const random_integer = std.crypto.random.int(u64);
const tmp_sub_path = std.fmt.hex(random_integer);
const file = dir.createFile(&tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
};
return .{
.file_writer = file.writer(write_buffer),
.tmp_path_buf = tmp_path_buf,
.random_integer = random_integer,
.dest_basename = dest_basename,
.file_open = true,
.file_exists = true,
@ -54,33 +44,51 @@ pub fn init(
}
/// Always call deinit, even after a successful finish().
pub fn deinit(self: *AtomicFile) void {
if (self.file_open) {
self.file_writer.file.close();
self.file_open = false;
pub fn deinit(af: *AtomicFile) void {
if (af.file_open) {
af.file_writer.file.close();
af.file_open = false;
}
if (self.file_exists) {
self.dir.deleteFile(&self.tmp_path_buf) catch {};
self.file_exists = false;
if (af.file_exists) {
const tmp_sub_path = std.fmt.hex(af.random_integer);
af.dir.deleteFile(&tmp_sub_path) catch {};
af.file_exists = false;
}
if (self.close_dir_on_deinit) {
self.dir.close();
if (af.close_dir_on_deinit) {
af.dir.close();
}
self.* = undefined;
af.* = undefined;
}
pub const FinishError = posix.RenameError;
pub const FlushError = File.WriteError;
pub fn flush(af: *AtomicFile) FlushError!void {
af.file_writer.interface.flush() catch |err| switch (err) {
error.WriteFailed => return af.file_writer.err.?,
};
}
pub const RenameIntoPlaceError = posix.RenameError;
/// On Windows, this function introduces a period of time where some file
/// system operations on the destination file will result in
/// `error.AccessDenied`, including rename operations (such as the one used in
/// this function).
pub fn finish(self: *AtomicFile) FinishError!void {
assert(self.file_exists);
if (self.file_open) {
self.file_writer.file.close();
self.file_open = false;
pub fn renameIntoPlace(af: *AtomicFile) RenameIntoPlaceError!void {
assert(af.file_exists);
if (af.file_open) {
af.file_writer.file.close();
af.file_open = false;
}
try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename);
self.file_exists = false;
const tmp_sub_path = std.fmt.hex(af.random_integer);
try posix.renameat(af.dir.fd, &tmp_sub_path, af.dir.fd, af.dest_basename);
af.file_exists = false;
}
pub const FinishError = FlushError || RenameIntoPlaceError;
/// Combination of `flush` followed by `renameIntoPlace`.
pub fn finish(af: *AtomicFile) FinishError!void {
try af.flush();
try af.renameIntoPlace();
}

View File

@ -2667,12 +2667,11 @@ pub fn copyFile(
});
defer atomic_file.deinit();
const size = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
_ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
error.ReadFailed => return file_reader.err.?,
error.WriteFailed => return atomic_file.file_writer.err.?,
};
try atomic_file.finish();
_ = size;
}
pub const AtomicFileOptions = struct {