const std = @import("std"); const os = @import("os.zig"); const Content = @This(); const log = std.log.scoped(.Content); pub const Digest = [64]u8; io: std.Io, arena: *std.heap.ArenaAllocator, root: std.Io.Dir, pub fn init(io: std.Io, allocator: std.mem.Allocator, root_dir: std.Io.Dir) !Content { const arena = try allocator.create(std.heap.ArenaAllocator); errdefer allocator.destroy(arena); arena.* = std.heap.ArenaAllocator.init(allocator); return .{ .io = io, .arena = arena, .root = root_dir, }; } pub fn deinit(self: *Content) void { self.root.close(self.io); const allocator = self.arena.child_allocator; self.arena.deinit(); allocator.destroy(self.arena); } pub fn digest(self: *Content, dir: std.Io.Dir, sub_path: []const u8) !Digest { // generate sha256 digest // TODO: use zig-based hashing? const command = blk: { var buf: [std.os.linux.PATH_MAX + 10]u8 = undefined; @memcpy(buf[0..10], "sha256sum "); @memcpy(buf[10 .. 10 + sub_path.len], sub_path); break :blk buf[0 .. 10 + sub_path.len]; }; var buf: [std.os.linux.PATH_MAX + 64]u8 = undefined; var w = std.Io.Writer.fixed(&buf); try os.captureStdout(self.io, command, .{ .cwd = .{ .dir = dir } }, &w); // take only the digest return buf[0..64].*; } pub fn get(self: *Content, dgst: Digest) !?Digest { const found = blk: { self.root.access(self.io, &dgst, .{}) catch |err| switch (err) { error.FileNotFound => break :blk false, else => return err, }; break :blk true; }; if (found) { return dgst; } else { return null; } } pub fn add(self: *Content, dir: std.Io.Dir, sub_path: []const u8) !Digest { const dgst = try self.digest(dir, sub_path); return (try self.get(dgst)) orelse { // create digest directory try self.root.createDirPath(self.io, &dgst); log.debug("Added {s}", .{dgst}); return dgst; }; } pub fn del(self: *Content, dgst: Digest) !void { try self.root.deleteTree(self.io, &dgst); } const Enumerator = struct { content: *Content, walker: std.Io.Dir.SelectiveWalker, pub fn deinit(self: *Enumerator) void { self.walker.deinit(); } pub fn next(self: *Enumerator) !?Digest { if (try self.walker.next(self.content.io)) |e| { switch (e.kind) { // TODO: we should probably check that this is actually a digest .directory => return e.basename[0..64].*, else => return null, } } else { return null; } } }; pub fn enumerate(self: *Content, allocator: std.mem.Allocator) !Enumerator { return .{ .walker = try self.root.walkSelectively(allocator), .content = self, }; } pub fn propSet(self: *Content, dgst: Digest, key: []const u8, val: ?[]const u8) !void { const digest_dir = try self.root.openDir(self.io, &dgst, .{}); defer digest_dir.close(self.io); if (val == null) { return try digest_dir.deleteFile(self.io, key); } return try digest_dir.writeFile(self.io, .{ .sub_path = key, .data = val.?, }); } pub fn propGet(self: *Content, dgst: Digest, key: []const u8, allocator: std.mem.Allocator) !?[]const u8 { const digest_dir = try self.root.openDir(self.io, &dgst, .{}); defer digest_dir.close(self.io); if (digest_dir.readFileAlloc(self.io, key, allocator, .unlimited)) |val| { return val; } else |err| switch (err) { error.FileNotFound => return null, else => return err, } } // fn checkLink(self: *Content, allocator: std.mem.Allocator, dgst: Digest) ![]const u8 { // const link_target = try self.propGet(dgst, "link", allocator); // // check target exists // try std.Io.Dir.cwd().access(self.io, link_target, .{ .read = true }); // return link_target; // } test "e2e" { std.testing.log_level = .debug; const DEMO_DIR: []const u8 = "/home/robert/tmp/rbc-demo/"; const CONTENT_DIR: []const u8 = DEMO_DIR ++ ".content"; try std.Io.Dir.cwd().deleteTree(std.testing.io, CONTENT_DIR); const demo_dir = try std.Io.Dir.cwd().openDir(std.testing.io, DEMO_DIR, .{ .iterate = true }); const content_dir = try std.Io.Dir.cwd().createDirPathOpen(std.testing.io, CONTENT_DIR, .{ .open_options = .{ .iterate = true } }); var cnt = try Content.init(std.testing.io, std.testing.allocator, content_dir); defer cnt.deinit(); { var walker = try demo_dir.walkSelectively(std.testing.allocator); defer walker.deinit(); while (try walker.next(std.testing.io)) |e| { switch (e.kind) { .file => { log.info("+ {s}", .{e.path}); const dgst = try cnt.add(demo_dir, e.path); try content_dir.access(std.testing.io, &dgst, .{}); const rel = try std.fs.path.relativePosix(std.testing.allocator, DEMO_DIR, CONTENT_DIR, e.path); defer std.testing.allocator.free(rel); try cnt.propSet(dgst, "link", rel); }, else => {}, } } } var etor = try cnt.enumerate(std.testing.allocator); defer etor.deinit(); { const dgst = try etor.next(); try std.testing.expect(dgst != null); try cnt.propSet(dgst.?, "foo", "bar"); const val1 = try cnt.propGet(dgst.?, "foo", std.testing.allocator); try std.testing.expect(val1 != null); defer std.testing.allocator.free(val1); try std.testing.expect(std.mem.eql(u8, "bar", val1.?)); try cnt.propSet(dgst.?, "foo", null); const val2 = try cnt.propGet(dgst.?, "foo", std.testing.allocator); try std.testing.expect(val2 == null); } { const dgst = try etor.next(); try std.testing.expect(dgst != null); try cnt.del(dgst.?); const err = content_dir.access(std.testing.io, &dgst.?, .{}); try std.testing.expectError(error.FileNotFound, err); } while (try etor.next()) |dgst| { log.info("? {s}", .{dgst}); try content_dir.access(std.testing.io, &dgst, .{}); const link = try cnt.propGet(dgst, "link", std.testing.allocator); try std.testing.expect(link != null); defer std.testing.allocator.free(link.?); try content_dir.access(std.testing.io, link.?, .{}); log.info("l {s}", .{link.?}); } }