about summary refs log tree commit diff
path: root/src/Content.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Content.zig')
-rw-r--r--src/Content.zig210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/Content.zig b/src/Content.zig
new file mode 100644
index 0000000..120d8b5
--- /dev/null
+++ b/src/Content.zig
@@ -0,0 +1,210 @@
+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.?});
+    }
+}