อิกัวนาต้นฉบับกำลังย้ายกล่องปิดผนึกไปยังช่องว่าง ขณะที่ช่องปลายทางอีกช่องมีแผ่นกั้นป้องกันการเขียนทับ

Zig 0.16 renamePreserve: ย้ายไฟล์โดยไม่เขียนทับปลายทางเงียบ ๆ

พี่โชว์Engineering

งาน publish ไฟล์มักเขียนลงชื่อชั่วคราวแล้ว rename ไปชื่อจริง แต่ rename ทั่วไปอาจแทนที่ไฟล์ปลายทางได้ครับ Zig 0.16 เพิ่ม Io.Dir.renamePreserve: ถ้าชื่อปลายทางมีอยู่แล้วจะคืน error.PathAlreadyExists แทนการเขียนทับ เหมือนพนักงานคลังหยุดรถเข็นเมื่อช่องมีของ ไม่ดันของเดิมตกพื้น

const std = @import("std");

test "renamePreserve refuses to overwrite" {
    const io = std.testing.io;
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();

    var draft = try tmp.dir.createFile(io, "draft", .{});
    draft.close(io);
    var current = try tmp.dir.createFile(io, "current", .{});
    current.close(io);

    try std.testing.expectError(
        error.PathAlreadyExists,
        tmp.dir.renamePreserve("draft", tmp.dir, "current", io),
    );
    try tmp.dir.renamePreserve("draft", tmp.dir, "published", io);
    try tmp.dir.access(io, "published", .{});
}

Preserve ไม่ใช่ replace

ใช้วิธีนี้เมื่อ “ชื่อปลายทางยังว่าง” เป็น invariant เช่นสร้าง artifact ที่ต้อง unique หรือ materialize cache entry ครั้งแรก หากนโยบายคืออัปเดตไฟล์เดิมโดยตั้งใจ ต้องใช้ flow แบบ atomic replace ที่ประกาศเจตนาชัด ไม่ควร catch PathAlreadyExists แล้วลบของเดิมทันที เพราะจะเปิดช่องว่างระหว่าง delete กับ rename และทำลายข้อมูลที่ process อื่นเพิ่งสร้าง

API ยังอาจคืน OperationUnsupported เมื่อระบบไฟล์ไม่มี primitive สำหรับ atomic non-replacing rename และอาจได้ CrossDevice เมื่อต้นทางกับปลายทางคนละ filesystem ดังนั้น production code ต้องกำหนด fallback เอง ไม่ควรตีความทุก error ว่า retry ได้ ตัวอย่างทดสอบใน temporary directory เดียวกันเพื่อล็อก contract เรื่องไม่ overwrite โดยไม่แตะไฟล์จริงของ repository

Recap

  • ใช้ renamePreserve เมื่อการมีปลายทางอยู่แล้วคือ conflict
  • จัดการ PathAlreadyExists, OperationUnsupported และ CrossDevice แยกกัน
  • อย่าลบปลายทางเพื่อทำให้ preserve กลายเป็น replace
  • เขียนไฟล์ชั่วคราวบน filesystem เดียวกับปลายทางเมื่อหวัง atomicity