概述
我们的第三个项目将文件I/O提升到了一个新的水平:构建一个小型、健壮的文件复制器,它默认是安全的,能发出清晰的诊断信息,并能自行清理。我们将把第四章的defer/errdefer模式与真实世界的错误处理联系起来,同时展示标准库的原子复制助手;参见04和Dir.zig。
两种方法说明了其中的权衡:
- 高级方法:单次调用
std.fs.Dir.copyFile执行原子复制并保留文件模式。 - 手动流式处理:使用
defer和errdefer打开、读取和写入,如果出现任何失败,则删除部分输出,具体如#defer和errdefer及File.zig中所述。
学习目标
- 设计一个CLI,除非明确强制,否则拒绝覆盖现有文件,如#命令行标志中所述。
- 使用
defer/errdefer来保证资源清理并在失败时移除部分文件。 - 在原子便利性的
Dir.copyFile和用于细粒度控制的手动流式处理之间进行选择。
正确性优先:默认安全的CLI
破坏用户数据是不可原谅的。这个工具采取保守立场:除非提供了--force,否则现有的目标会中止复制。我们还验证了源是常规文件,并在成功时保持stdout静默,这样脚本就可以将“无输出”视为好兆头,具体如#错误处理中所述。
在现有目标上中止
我们首先探测目标路径。如果存在且未提供--force,我们会打印单行诊断信息并以非零状态退出。这反映了常见的Unix实用程序,并使失败变得明确。
一次调用完成原子复制
尽可能利用标准库。Dir.copyFile使用一个临时文件并将其重命名到位,这意味着即使进程在复制中途崩溃,调用者也永远不会观察到部分写入的目标。文件模式默认保留;如果你需要时间戳,可以使用下面提到的updateFile来处理。
const std = @import("std");
// Chapter 7 – Safe File Copier (atomic via std.fs.Dir.copyFile)
//
// A minimal, safe-by-default CLI that refuses to clobber an existing
// destination unless --force is provided. Uses std.fs.Dir.copyFile,
// which writes to a temporary file and atomically renames it into place.
//
// Usage:
// zig run safe_copy.zig -- <src> <dst>
// zig run safe_copy.zig -- --force <src> <dst>
// 第7章 - 安全文件复制器(通过std.fs.Dir.copyFile实现原子性)
//
// 一个最小化、默认安全的CLI,拒绝覆盖现有目标文件,
// 除非提供--force参数。使用std.fs.Dir.copyFile,
// 它将数据写入临时文件并原子性地重命名到目标位置。
//
// 用法:
// zig run safe_copy.zig -- <src> <dst>
// zig run safe_copy.zig -- --force <src> <dst>
const Cli = struct {
force: bool = false,
src: []const u8 = &[_]u8{},
dst: []const u8 = &[_]u8{},
};
fn printUsage() void {
std.debug.print("usage: safe-copy [--force] <source> <dest>\n", .{});
}
fn parseArgs(allocator: std.mem.Allocator) !Cli {
var cli: Cli = .{};
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) {
printUsage();
std.process.exit(0);
}
var i: usize = 1;
while (i < args.len and std.mem.startsWith(u8, args[i], "--")) : (i += 1) {
const flag = args[i];
if (std.mem.eql(u8, flag, "--force")) {
cli.force = true;
} else if (std.mem.eql(u8, flag, "--help")) {
printUsage();
std.process.exit(0);
} else {
std.debug.print("error: unknown flag '{s}'\n", .{flag});
printUsage();
std.process.exit(2);
}
}
const remaining = args.len - i;
if (remaining != 2) {
std.debug.print("error: expected <source> and <dest>\n", .{});
printUsage();
std.process.exit(2);
}
// Duplicate paths so they remain valid after freeing args.
// 复制路径以确保释放args后路径仍然有效。
cli.src = try allocator.dupe(u8, args[i]);
cli.dst = try allocator.dupe(u8, args[i + 1]);
return cli;
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const cli = try parseArgs(allocator);
const cwd = std.fs.cwd();
// Validate that source exists and is a regular file.
// 验证源文件存在且为常规文件。
var src_file = cwd.openFile(cli.src, .{ .mode = .read_only }) catch {
std.debug.print("error: unable to open source '{s}'\n", .{cli.src});
std.process.exit(1);
};
defer src_file.close();
const st = try src_file.stat();
if (st.kind != .file) {
std.debug.print("error: source is not a regular file\n", .{});
std.process.exit(1);
}
// Respect safe-by-default semantics: refuse to overwrite unless --force.
// 遵循默认安全的语义:除非提供--force,否则拒绝覆盖。
const dest_exists = blk: {
_ = cwd.statFile(cli.dst) catch |err| switch (err) {
error.FileNotFound => break :blk false,
else => |e| return e,
};
break :blk true;
};
if (dest_exists and !cli.force) {
std.debug.print("error: destination exists; pass --force to overwrite\n", .{});
std.process.exit(2);
}
// Perform an atomic copy preserving mode by default. On success, there is
// intentionally no output to keep pipelines quiet and scripting-friendly.
// 执行原子复制并默认保留文件模式。成功时,
// 有意不输出任何内容,以保持管道的安静和脚本友好性。
cwd.copyFile(cli.src, cwd, cli.dst, .{ .override_mode = null }) catch |err| {
std.debug.print("error: copy failed ({s})\n", .{@errorName(err)});
std.process.exit(1);
};
}
$ printf 'hello, copier!\n' > from.txt
$ zig run safe_copy.zig -- from.txt to.txt(无输出)copyFile会覆盖现有文件。我们的包装器首先检查是否存在,并需要--force才能覆盖。如果你还想保留atime/mtime,请优先使用Dir.updateFile。
有意覆盖
当输出已存在时,演示显式覆盖:
$ printf 'v1\n' > from.txt
$ printf 'old\n' > to.txt
$ zig run safe_copy.zig -- from.txt to.txt
error: destination exists; pass --force to overwrite
$ zig run safe_copy.zig -- --force from.txt to.txterror: destination exists; pass --force to overwrite
(无输出)成功在设计上保持静默;与echo $?结合使用,以便在脚本中消费状态码。
使用defer/errdefer进行手动流式处理
为了进行细粒度控制(或作为学习练习),将Reader连接到Writer并自己流式传输字节。关键之处在于使用errdefer在创建后如果出现任何问题就移除目标——这可以防止留下被截断的文件。
const std = @import("std");
// Chapter 7 – Safe File Copier (manual streaming with errdefer cleanup)
//
// Demonstrates opening, reading, writing, and cleaning up safely using
// defer/errdefer. If the copy fails after destination creation, we remove
// the partial file so callers never observe a truncated artifact.
//
// Usage:
// zig run copy_stream.zig -- <src> <dst>
// zig run copy_stream.zig -- --force <src> <dst>
// 第7章 - 安全文件复制器(使用errdefer清理的手动流式处理)
//
// 演示使用defer/errdefer进行安全的打开、读取、写入和清理。
// 如果在创建目标文件后复制失败,我们将删除部分文件,
// 以便调用者永远观察不到截断的产物。
//
// 用法:
// zig run copy_stream.zig -- <src> <dst>
// zig run copy_stream.zig -- --force <src> <dst>
const Cli = struct {
force: bool = false,
src: []const u8 = &[_]u8{},
dst: []const u8 = &[_]u8{},
};
fn printUsage() void {
std.debug.print("usage: copy-stream [--force] <source> <dest>\n", .{});
}
fn parseArgs(allocator: std.mem.Allocator) !Cli {
var cli: Cli = .{};
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) {
printUsage();
std.process.exit(0);
}
var i: usize = 1;
while (i < args.len and std.mem.startsWith(u8, args[i], "--")) : (i += 1) {
const flag = args[i];
if (std.mem.eql(u8, flag, "--force")) {
cli.force = true;
} else if (std.mem.eql(u8, flag, "--help")) {
printUsage();
std.process.exit(0);
} else {
std.debug.print("error: unknown flag '{s}'\n", .{flag});
printUsage();
std.process.exit(2);
}
}
const remaining = args.len - i;
if (remaining != 2) {
std.debug.print("error: expected <source> and <dest>\n", .{});
printUsage();
std.process.exit(2);
}
// Duplicate paths so they remain valid after freeing args.
// 复制路径以确保释放args后路径仍然有效。
cli.src = try allocator.dupe(u8, args[i]);
cli.dst = try allocator.dupe(u8, args[i + 1]);
return cli;
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const cli = try parseArgs(allocator);
const cwd = std.fs.cwd();
// Open source and inspect its metadata.
// 打开源文件并检查其元数据。
var src = cwd.openFile(cli.src, .{ .mode = .read_only }) catch {
std.debug.print("error: unable to open source '{s}'\n", .{cli.src});
std.process.exit(1);
};
defer src.close();
const st = try src.stat();
if (st.kind != .file) {
std.debug.print("error: source is not a regular file\n", .{});
std.process.exit(1);
}
// Safe-by-default: refuse to overwrite unless --force.
// 默认安全:除非提供--force,否则拒绝覆盖。
if (!cli.force) {
const dest_exists = blk: {
_ = cwd.statFile(cli.dst) catch |err| switch (err) {
error.FileNotFound => break :blk false,
else => |e| return e,
};
break :blk true;
};
if (dest_exists) {
std.debug.print("error: destination exists; pass --force to overwrite\n", .{});
std.process.exit(2);
}
}
// Create destination with exclusive mode when not forcing overwrite.
// 在不强制覆盖时以独占模式创建目标文件。
var dest = cwd.createFile(cli.dst, .{
.read = false,
.truncate = cli.force,
.exclusive = !cli.force,
.mode = st.mode,
}) catch |err| switch (err) {
error.PathAlreadyExists => {
std.debug.print("error: destination exists; pass --force to overwrite\n", .{});
std.process.exit(2);
},
else => |e| {
std.debug.print("error: cannot create destination ({s})\n", .{@errorName(e)});
std.process.exit(1);
},
};
// Ensure closure and cleanup order: close first, then delete on error.
// 确保关闭和清理的顺序:先关闭,出错时再删除。
defer dest.close();
errdefer cwd.deleteFile(cli.dst) catch {};
// Wire a Reader/Writer pair and copy using the Writer interface.
// 连接Reader/Writer对并使用Writer接口进行复制。
var reader: std.fs.File.Reader = .initSize(src, &.{}, st.size);
var write_buf: [64 * 1024]u8 = undefined; // buffered writes
// 缓冲写入
var writer = std.fs.File.writer(dest, &write_buf);
_ = writer.interface.sendFileAll(&reader, .unlimited) catch |err| switch (err) {
error.ReadFailed => return reader.err.?,
error.WriteFailed => return writer.err.?,
};
// Flush buffered bytes and set the final file length.
// 冲刷缓冲的字节并设置最终的文件长度。
try writer.end();
}
$ printf 'stream me\n' > src.txt
$ zig run copy_stream.zig -- src.txt dst.txt(无输出)使用.exclusive = true创建目标时,如果文件已存在,打开操作将失败。这加上errdefer deleteFile,在典型的单进程场景中提供了强大的安全保证,而不会产生竞争条件。
注意与警告
- 原子语义:
Dir.copyFile创建一个临时文件并将其重命名到位,从而避免了其他进程的部分读取。在旧的Linux内核上,断电可能会留下一个临时文件;详情请参见函数的文档注释。 - 保留时间戳:当你需要atime/mtime与源文件匹配时,除了内容和模式,请优先使用
Dir.updateFile。 - 性能提示:
Writer接口在可用时使用平台加速(sendfile、copy_file_range或fcopyfile),否则回退到缓冲循环;参见posix.zig。 - CLI生命周期:在释放
args之前复制其字符串,以避免悬空的[]u8切片(两个例子都使用allocator.dupe);参见process.zig。 - 健全性检查:首先打开源文件,然后对其进行
stat(),并要求kind == .file以拒绝目录和特殊文件。
练习
- 添加一个
--no-clobber标志,即使同时存在--force也强制报错——然后发出一个有用的消息,建议移除其中一个。 - 通过切换到
Dir.updateFile并用stat验证时间戳是否匹配来实现--preserve-times。 - 使用
CopyFileOptions.override_mode教工具从数字模式覆盖(例如--mode=0644)复制文件权限。
替代方案和边缘情况:
- 在这些示例中,有意拒绝复制特殊文件(目录、fifo、设备);请明确处理或跳过它们。
- 跨文件系统移动:当设备不同时,复制加
deleteFile比rename更安全;Zig的助手在给定内容副本的情况下会做正确的事情。 - 非常大的文件:首先选择高级复制;如果你不使用
Writer接口,手动循环应分块读取并小心处理短写。