╭ Attachment.zig ───────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const hash = xit.hash; 6 7 parent_id: [evt.event_id_size * 2]u8, 8 9 // what the db stores: the event's data plus the commit-derived fields. the file 10 // itself is the one entry in the event commit's tree, so its name and blob come 11 // from there rather than from the payload. 12 pub const Record = struct { 13 event: Self, 14 removed: bool = false, 15 author_email: ?[]const u8 = null, 16 created_ts: u64 = 0, // the commit timestamp of the event that created this attachment 17 name: []const u8, 18 blob_oid: []const u8, 19 }; 20 21 const Self = @This(); 22 23 // the moment keys `evt.merge` reads and writes for this kind 24 pub const merge_policy: evt.MergePolicy = .target_wins; 25 pub const record_map_key = "event-id->attachment"; 26 pub const id_set_key = "attachment-id-set"; 27 28 // the index a parent event's view reads 29 pub const parent_id_to_attachment_id_set_key = "parent-id->attachment-id-set"; 30 31 pub const max_size: usize = 10 * 1024 * 1024; 32 pub const name_max_len = 255; 33 34 // the name is the tree entry's name, so it must be a legal path component 35 pub fn nameValid(name: []const u8) bool { 36 if (name.len == 0 or name.len > name_max_len) return false; 37 if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return false; 38 for (name) |ch| { 39 if (ch == '/' or ch == '\\' or std.ascii.isControl(ch)) return false; 40 } 41 return true; 42 } 43 44 fn parentExists( 45 comptime DB: type, 46 comptime hash_kind: hash.HashKind, 47 haxy_moment: anytype, 48 parent_id: *const [evt.event_id_size]u8, 49 ) !bool { 50 const kinds_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.event_index_key)) orelse return false; 51 const kinds = try DB.HashMap(.read_only).init(kinds_cursor); 52 return try kinds.getCursor(hash.hashInt(hash_kind, parent_id)) != null; 53 } 54 55 pub fn consume( 56 comptime DB: type, 57 comptime hash_kind: hash.HashKind, 58 haxy_moment: DB.HashMap(.read_write), 59 event_id: *const [evt.event_id_size]u8, 60 record_maybe: ?Record, 61 arena: *std.heap.ArenaAllocator, 62 _: ?[]const u8, 63 ) !void { 64 const attachment_key = hash.hashInt(hash_kind, event_id); 65 66 const event_id_to_attachment_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key)); 67 const event_id_to_attachment = try DB.HashMap(.read_write).init(event_id_to_attachment_cursor); 68 69 const parent_id_to_attachment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, parent_id_to_attachment_id_set_key)); 70 const parent_id_to_attachment_id_set = try DB.HashMap(.read_write).init(parent_id_to_attachment_id_set_cursor); 71 72 var existing_record_maybe: ?Record = null; 73 const existing_cursor_maybe = try event_id_to_attachment.getCursor(attachment_key); 74 if (existing_cursor_maybe) |existing_cursor| { 75 const existing_attachment = try DB.HashMap(.read_only).init(existing_cursor); 76 existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_attachment); 77 } 78 79 var record_to_write = if (record_maybe) |record| 80 record 81 else blk: { 82 var record = existing_record_maybe orelse return error.EventNotFound; 83 record.removed = true; 84 break :blk record; 85 }; 86 87 // references use the same lower-case hex form as event ids in json 88 var id_bytes: [evt.event_id_size]u8 = undefined; 89 _ = try std.fmt.hexToBytes(&id_bytes, &record_to_write.event.parent_id); 90 if (!std.mem.eql(u8, &record_to_write.event.parent_id, &std.fmt.bytesToHex(id_bytes, .lower))) return error.InvalidEventId; 91 if (!try parentExists(DB, hash_kind, haxy_moment, &id_bytes)) return error.ParentNotFound; 92 if (!nameValid(record_to_write.name)) return error.InvalidAttachment; 93 94 if (existing_record_maybe) |existing_record| { 95 // the file never changes, so an update preserves everything the original 96 // commit determined 97 record_to_write.created_ts = existing_record.created_ts; 98 record_to_write.author_email = existing_record.author_email; 99 record_to_write.name = existing_record.name; 100 record_to_write.blob_oid = existing_record.blob_oid; 101 102 // an attachment cannot move to another parent 103 if (!std.mem.eql(u8, &existing_record.event.parent_id, &record_to_write.event.parent_id)) return error.ParentChanged; 104 } 105 106 const attachment_cursor = try event_id_to_attachment.putCursor(attachment_key); 107 const attachment = try DB.HashMap(.read_write).init(attachment_cursor); 108 try evt.upsert(Record, DB, hash_kind, attachment, record_to_write); 109 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .attach, record_to_write.created_ts, record_to_write.removed); 110 111 if (existing_cursor_maybe == null) { 112 const attachment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); 113 const attachment_id_set = try DB.SortedSet(.read_write).init(attachment_id_set_cursor); 114 try attachment_id_set.put(&evt.orderKeyDesc(record_to_write.created_ts, event_id)); 115 } 116 117 // removed attachments stay in the parent's set; the views skip them 118 const parent_attachments_cursor = try parent_id_to_attachment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.parent_id)); 119 const parent_attachments = try DB.SortedSet(.read_write).init(parent_attachments_cursor); 120 try parent_attachments.put(&evt.orderKey(record_to_write.created_ts, event_id)); 121 } 122 123 // attach `blob` to a parent event and return the event id. `repo` must be 124 // writable. 125 pub fn create( 126 comptime repo_kind: rp.RepoKind, 127 comptime repo_opts: rp.RepoOpts(repo_kind), 128 io: std.Io, 129 allocator: std.mem.Allocator, 130 repo: *rp.Repo(repo_kind, repo_opts), 131 parent_id: *const [evt.event_id_size * 2]u8, 132 blob: evt.Blob, 133 author: evt.CommitAuthor, 134 ) ![evt.event_id_size * 2]u8 { 135 if (!nameValid(blob.name)) return error.InvalidFields; 136 if (blob.size == 0 or blob.size > max_size) return error.InvalidFields; 137 138 var parent_id_bytes: [evt.event_id_size]u8 = undefined; 139 _ = std.fmt.hexToBytes(&parent_id_bytes, parent_id) catch return error.InvalidFields; 140 if (!std.mem.eql(u8, parent_id, &std.fmt.bytesToHex(parent_id_bytes, .lower))) return error.InvalidFields; 141 142 { 143 const DB = evt.EventDB(repo_opts.hash); 144 var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null; 145 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 146 const haxy_moment = (if (event_db_maybe) |*event_db| 147 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 148 else if (repo_kind == .git) 149 return error.ParentNotFound 150 else 151 evt.currentMoment(repo_opts, repo)) catch return error.ParentNotFound; 152 if (!try parentExists(DB, repo_opts.hash, haxy_moment, &parent_id_bytes)) return error.ParentNotFound; 153 } 154 155 var id_bytes: [evt.event_id_size]u8 = undefined; 156 io.random(&id_bytes); 157 const event_id = std.fmt.bytesToHex(id_bytes, .lower); 158 try evt.consume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 159 .id = event_id, 160 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 161 .author = author, 162 .blob = blob, 163 .event = .{ .attach = .{ .parent_id = parent_id.* } }, 164 }}); 165 return event_id; 166 } 167 168 // read an attachment by event id, or null if the id isn't a known attachment. 169 // field byte slices are allocated in `arena`. 170 pub fn readById( 171 comptime DB: type, 172 comptime hash_kind: hash.HashKind, 173 haxy_moment: DB.HashMap(.read_only), 174 arena: *std.heap.ArenaAllocator, 175 attachment_id: []const u8, 176 ) !?Record { 177 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 178 const records = try DB.HashMap(.read_only).init(records_cursor); 179 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, attachment_id)) orelse return null; 180 const record_map = try DB.HashMap(.read_only).init(record_cursor); 181 return try evt.read(Record, DB, hash_kind, arena, record_map); 182 } 183