authorgravatar for gereeter+code@gmail.comJonathan S <gereeter+code@gmail.com> 2020-03-25 22:17:41-05:00
committergravatar for gereeter+code@gmail.comJonathan S <gereeter+code@gmail.com> 2020-03-25 23:22:36-05:00
loga779a96d38883c8d642fc9164c3a60165078c2d2
tree9e0e0f892f0a1304cfda7df53d8311e02cfc9ac7
parentf7f563ea53cf58c772003a46624b87dad9c4311d

In AtomicFile, work relative to the destination's parent directory. This is more robust against concurrent filesystem reorganization and avoids path length issues.


1 files changed, 21 insertions(+), 20 deletions(-)

lib/std/fs.zig+21-20
...@@ -118,38 +118,30 @@ pub fn copyFileAbsolute(source_path: []const u8, dest_path: []const u8, args: Co...@@ -118,38 +118,30 @@ pub fn copyFileAbsolute(source_path: []const u8, dest_path: []const u8, args: Co
118/// TODO update this API to avoid a getrandom syscall for every operation.118/// TODO update this API to avoid a getrandom syscall for every operation.
119pub const AtomicFile = struct {119pub const AtomicFile = struct {
120 file: File,120 file: File,
121 tmp_path_buf: [MAX_PATH_BYTES - 1:0]u8,121 // TODO either replace this with rand_buf or use []u16 on Windows
122 tmp_path_buf: [TMP_PATH_LEN:0]u8,
122 dest_path: []const u8,123 dest_path: []const u8,
123 file_open: bool,124 file_open: bool,
124 file_exists: bool,125 file_exists: bool,
126 close_dir_on_deinit: bool,
125 dir: Dir,127 dir: Dir,
126128
127 const InitError = File.OpenError;129 const InitError = File.OpenError;
128130
131 const TMP_PATH_LEN = base64.Base64Encoder.calcSize(12);
132
129 /// TODO rename this. Callers should go through Dir API133 /// TODO rename this. Callers should go through Dir API
130 pub fn init2(dest_path: []const u8, mode: File.Mode, dir: Dir) InitError!AtomicFile {134 pub fn init2(dest_path: []const u8, mode: File.Mode, dir: Dir, close_dir_on_deinit: bool) InitError!AtomicFile {
131 const dirname = path.dirname(dest_path);
132 var rand_buf: [12]u8 = undefined;135 var rand_buf: [12]u8 = undefined;
133 const dirname_component_len = if (dirname) |d| d.len + 1 else 0;136 var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined;
134 const encoded_rand_len = comptime base64.Base64Encoder.calcSize(rand_buf.len);137 tmp_path_buf[base64.Base64Encoder.calcSize(12)] = 0;
135 const tmp_path_len = dirname_component_len + encoded_rand_len;
136 var tmp_path_buf: [MAX_PATH_BYTES - 1:0]u8 = undefined;
137 if (tmp_path_len > tmp_path_buf.len) return error.NameTooLong;
138
139 if (dirname) |dn| {
140 mem.copy(u8, tmp_path_buf[0..], dn);
141 tmp_path_buf[dn.len] = path.sep;
142 }
143
144 tmp_path_buf[tmp_path_len] = 0;
145 const tmp_path_slice = tmp_path_buf[0..tmp_path_len :0];
146138
147 while (true) {139 while (true) {
148 try crypto.randomBytes(rand_buf[0..]);140 try crypto.randomBytes(rand_buf[0..]);
149 base64_encoder.encode(tmp_path_slice[dirname_component_len..tmp_path_len], &rand_buf);141 base64_encoder.encode(&tmp_path_buf, &rand_buf);
150142
151 const file = dir.createFileC(143 const file = dir.createFileC(
152 tmp_path_slice,144 &tmp_path_buf,
153 .{ .mode = mode, .exclusive = true },145 .{ .mode = mode, .exclusive = true },
154 ) catch |err| switch (err) {146 ) catch |err| switch (err) {
155 error.PathAlreadyExists => continue,147 error.PathAlreadyExists => continue,
...@@ -162,6 +154,7 @@ pub const AtomicFile = struct {...@@ -162,6 +154,7 @@ pub const AtomicFile = struct {
162 .dest_path = dest_path,154 .dest_path = dest_path,
163 .file_open = true,155 .file_open = true,
164 .file_exists = true,156 .file_exists = true,
157 .close_dir_on_deinit = close_dir_on_deinit,
165 .dir = dir,158 .dir = dir,
166 };159 };
167 }160 }
...@@ -169,7 +162,7 @@ pub const AtomicFile = struct {...@@ -169,7 +162,7 @@ pub const AtomicFile = struct {
169162
170 /// Deprecated. Use `Dir.atomicFile`.163 /// Deprecated. Use `Dir.atomicFile`.
171 pub fn init(dest_path: []const u8, mode: File.Mode) InitError!AtomicFile {164 pub fn init(dest_path: []const u8, mode: File.Mode) InitError!AtomicFile {
172 return init2(dest_path, mode, cwd());165 return cwd().atomicFile(dest_path, .{ .mode = mode });
173 }166 }
174167
175 /// always call deinit, even after successful finish()168 /// always call deinit, even after successful finish()
...@@ -182,6 +175,9 @@ pub const AtomicFile = struct {...@@ -182,6 +175,9 @@ pub const AtomicFile = struct {
182 self.dir.deleteFileC(&self.tmp_path_buf) catch {};175 self.dir.deleteFileC(&self.tmp_path_buf) catch {};
183 self.file_exists = false;176 self.file_exists = false;
184 }177 }
178 if (self.close_dir_on_deinit) {
179 self.dir.close();
180 }
185 self.* = undefined;181 self.* = undefined;
186 }182 }
187183
...@@ -1281,7 +1277,12 @@ pub const Dir = struct {...@@ -1281,7 +1277,12 @@ pub const Dir = struct {
1281 /// `dest_path` must remain valid for the lifetime of `AtomicFile`.1277 /// `dest_path` must remain valid for the lifetime of `AtomicFile`.
1282 /// Call `AtomicFile.finish` to atomically replace `dest_path` with contents.1278 /// Call `AtomicFile.finish` to atomically replace `dest_path` with contents.
1283 pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile {1279 pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile {
1284 return AtomicFile.init2(dest_path, options.mode, self);1280 if (path.dirname(dest_path)) |dirname| {
1281 const dir = try self.openDir(dirname, .{});
1282 return AtomicFile.init2(path.basename(dest_path), options.mode, dir, true);
1283 } else {
1284 return AtomicFile.init2(dest_path, options.mode, self, false);
1285 }
1285 }1286 }
1286};1287};
12871288