feat: add inspectFile and restoreFromFile to backup

This commit is contained in:
dualfroz committed 2026-09-01 20:18:13 +02:00
1 parent 70dc1b8283
commit f9da2b513b
1 file changed
+161
+161
View File
@@ -150,3 +150,164 @@ fn parse(allocator: Allocator, data: []const u8) !ParsedBackup {
var entries = std.ArrayList(ParsedEntry).init(allocator); var entries = std.ArrayList(ParsedEntry).init(allocator);
errdefer entries.deinit(); errdefer entries.deinit();
while (true) {
if (pos + footerMagic.len <= data.len and std.mem.eql(u8, data[pos .. pos + 4], footerMagic)) {
break;
}
if (pos + 8 > data.len) return BackupError.Truncated;
const keyLen = encode.readU32(data, pos);
const valueLen = encode.readU32(data, pos + 4);
const entryStart = pos;
const keyStart = pos + 8;
const valueStart = keyStart + keyLen;
const valueEnd = valueStart + valueLen;
if (valueEnd + 4 > data.len) return BackupError.Truncated;
const entryBytes = data[entryStart..valueEnd];
const storedCrc = encode.readU32(data, valueEnd);
const actualCrc = std.hash.Crc32.hash(entryBytes);
if (storedCrc != actualCrc) return BackupError.Corrupt;
try entries.append(.{ .key = data[keyStart..valueStart], .value = data[valueStart..valueEnd] });
pos = valueEnd + 4;
}
if (pos + 4 + 8 + 4 > data.len) return BackupError.Truncated;
const declaredCount = encode.readU64(data, pos + 4);
if (declaredCount != entries.items.len) return BackupError.CountMismatch;
const storedFileCrc = encode.readU32(data, pos + 4 + 8);
const actualFileCrc = std.hash.Crc32.hash(data[0..pos]);
if (storedFileCrc != actualFileCrc) return BackupError.Corrupt;
return .{ .seq = seq, .entries = try entries.toOwnedSlice() };
}
/// Reads and structurally validates a backup file without applying it,
/// returning its metadata (seq, entry count). Useful to verify a backup
/// before trusting it for disaster recovery.
pub fn inspectFile(allocator: Allocator, path: []const u8) !BackupInfo {
const data = try std.fs.cwd().readFileAlloc(allocator, path, 1 << 34);
defer allocator.free(data);
const parsed = try parse(allocator, data);
defer allocator.free(parsed.entries);
return .{ .seq = parsed.seq, .entryCount = parsed.entries.len };
}
/// Restores every entry from the backup at `path` into `db`, in chunks of
/// `chunkSize` applied atomically via WriteBatch. The whole file is validated
/// (magics, per-entry and whole-file crc, declared count) before any chunk is
/// applied, so a corrupt backup fails without mutating the target database.
/// Existing keys in `db` outside the backup are left untouched; keys present
/// in both are overwritten with the backup's value.
pub fn restoreFromFile(allocator: Allocator, db: *Db, path: []const u8, chunkSize: usize) !BackupInfo {
const data = try std.fs.cwd().readFileAlloc(allocator, path, 1 << 34);
defer allocator.free(data);
const parsed = try parse(allocator, data);
defer allocator.free(parsed.entries);
var i: usize = 0;
while (i < parsed.entries.len) {
const end = @min(i + chunkSize, parsed.entries.len);
var batch = WriteBatch.init(allocator);
defer batch.deinit();
for (parsed.entries[i..end]) |entry| {
try batch.put(entry.key, entry.value);
}
try db.apply(&batch);
i = end;
}
return .{ .seq = parsed.seq, .entryCount = parsed.entries.len };
}
test "backup round trip: dump then restore into a fresh db reproduces every key" {
const allocator = std.testing.allocator;
const srcDir = "test-backup-src-1";
const dstDir = "test-backup-dst-1";
const backupPath = "test-backup-1.cblb";
std.fs.cwd().deleteTree(srcDir) catch {};
std.fs.cwd().deleteTree(dstDir) catch {};
defer std.fs.cwd().deleteTree(srcDir) catch {};
defer std.fs.cwd().deleteTree(dstDir) catch {};
defer std.fs.cwd().deleteFile(backupPath) catch {};
var src = try Db.open(allocator, srcDir, .{});
defer src.close();
var keyBuf: [16]u8 = undefined;
var valBuf: [16]u8 = undefined;
var i: usize = 0;
while (i < 200) : (i += 1) {
const key = try std.fmt.bufPrint(&keyBuf, "k{d:0>6}", .{i});
const value = try std.fmt.bufPrint(&valBuf, "v{d:0>6}", .{i});
try src.put(key, value);
}
// overwrite one, delete another: backup must reflect resolved state.
try src.put("k000005", "updated");
try src.delete("k000010");
const snap = try src.snapshot();
const count = try dumpToFile(allocator, snap, backupPath);
try std.testing.expectEqual(@as(u64, 199), count);
var dst = try Db.open(allocator, dstDir, .{});
defer dst.close();
const info = try restoreFromFile(allocator, &dst, backupPath, 32);
try std.testing.expectEqual(@as(u64, 199), info.entryCount);
i = 0;
while (i < 200) : (i += 1) {
const key = try std.fmt.bufPrint(&keyBuf, "k{d:0>6}", .{i});
const got = try dst.get(allocator, key);
if (i == 10) {
try std.testing.expect(got == null);
continue;
}
defer allocator.free(got.?);
if (i == 5) {
try std.testing.expectEqualStrings("updated", got.?);
} else {
const expected = try std.fmt.bufPrint(&valBuf, "v{d:0>6}", .{i});
try std.testing.expectEqualStrings(expected, got.?);
}
}
}
test "backup of empty db round trips to zero entries" {
const allocator = std.testing.allocator;
const srcDir = "test-backup-src-empty";
const dstDir = "test-backup-dst-empty";
const backupPath = "test-backup-empty.cblb";
std.fs.cwd().deleteTree(srcDir) catch {};
std.fs.cwd().deleteTree(dstDir) catch {};
defer std.fs.cwd().deleteTree(srcDir) catch {};
defer std.fs.cwd().deleteTree(dstDir) catch {};
defer std.fs.cwd().deleteFile(backupPath) catch {};
var src = try Db.open(allocator, srcDir, .{});
defer src.close();
const snap = try src.snapshot();
const count = try dumpToFile(allocator, snap, backupPath);
try std.testing.expectEqual(@as(u64, 0), count);
const info = try inspectFile(allocator, backupPath);
try std.testing.expectEqual(@as(u64, 0), info.entryCount);
var dst = try Db.open(allocator, dstDir, .{});
defer dst.close();
_ = try restoreFromFile(allocator, &dst, backupPath, 32);
var iter = try dst.scan(allocator, null, null);
defer iter.deinit();
try std.testing.expect((try iter.next()) == null);
}
test "backup is point-in-time: writes after snapshot are excluded" {
const allocator = std.testing.allocator;
const srcDir = "test-backup-src-pit";
const backupPath = "test-backup-pit.cblb";
std.fs.cwd().deleteTree(srcDir) catch {};
defer std.fs.cwd().deleteTree(srcDir) catch {};
defer std.fs.cwd().deleteFile(backupPath) catch {};