const std = @import("std"); const c = @cImport({ @cInclude("time.h"); }); pub const FileSwapper = struct { allocator: std.mem.Allocator, target_path: []const u8, tmp_path: []const u8, tmp: std.fs.File, pub fn createAt(alloc: std.mem.Allocator, path: []const u8) !FileSwapper { const target_dir = std.fs.path.dirname(path) orelse "."; const target_name = std.fs.path.basename(path); const tmp_name = try std.fmt.allocPrint( alloc, ".{s}", .{target_name}, ); defer alloc.free(tmp_name); const tmp_path = try std.fs.path.join( alloc, &.{ target_dir, tmp_name }, ); const f = try std.fs.cwd().createFile(tmp_path, .{}); return .{ .allocator = alloc, .target_path = path, .tmp_path = tmp_path, .tmp = f, }; } pub fn writer(self: FileSwapper) std.fs.File.Writer { return self.tmp.writer(); } pub fn close(self: FileSwapper) void { defer self.allocator.free(self.tmp_path); defer self.tmp.close(); // flush writes self.tmp.sync() catch @panic("sync tmp file"); // check if target path exists var found = true; std.fs.cwd().access(self.target_path, .{}) catch |e| switch (e) { error.FileNotFound => found = false, else => @panic("unable to stat target path"), }; // swap file into final location std.fs.rename( std.fs.cwd(), self.tmp_path, std.fs.cwd(), self.target_path, ) catch @panic("rename tmp file"); } }; pub fn replaceExtension(alloc: std.mem.Allocator, p: []const u8, new: []const u8) ![]const u8 { const dirname = std.fs.path.dirname(p) orelse "."; const basename = try stripExtension(std.fs.path.basename(p)); return try joinPathAndFilename( alloc, &.{dirname}, &.{ basename, ".", new }, ); } // also trims off base paths pub fn stripExtension(path: []const u8) ![]const u8 { const ext = std.fs.path.extension(path); const ext_idx = std.mem.indexOfPosLinear(u8, path, 0, ext); std.debug.assert(ext_idx != null); return path[0..ext_idx.?]; } pub fn joinPathAndFilename(alloc: std.mem.Allocator, paths: []const []const u8, filename_parts: []const []const u8) ![]const u8 { const filename = try std.mem.concat(alloc, u8, filename_parts); defer alloc.free(filename); var p = try std.ArrayList([]const u8).initCapacity(alloc, paths.len + 1); defer p.deinit(); p.appendSliceAssumeCapacity(paths); p.appendAssumeCapacity(filename); const paths2 = try p.toOwnedSlice(); defer alloc.free(paths2); std.debug.assert(paths.len + 1 == paths2.len); return try std.fs.path.join(alloc, paths2); } pub fn getMtime(path: []const u8) !std.posix.timespec { const fd = try std.fs.cwd().openFile(path, .{ .mode = .read_only }); defer fd.close(); const meta = try fd.metadata(); const mtime = meta.modified(); return .{ .sec = std.math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse std.math.maxInt(isize), .nsec = std.math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse std.math.maxInt(isize), }; } // use libc strftime to format time string pub fn formatMtime(alloc: std.mem.Allocator, mtime: std.posix.timespec, fmt: [*c]const u8) ![]const u8 { const mtime_str = try alloc.alloc(u8, std.os.linux.PATH_MAX); var tm: c.struct_tm = undefined; _ = c.localtime_r(&mtime.sec, &tm); const rc = c.strftime( mtime_str.ptr, mtime_str.len, fmt, &tm, ); switch (std.posix.errno(rc)) { .SUCCESS => {}, else => |err| return std.posix.unexpectedErrno(err), } return try alloc.realloc(mtime_str, rc); }