tiny.cook.Store
Defined in tiny.cook.
API (7)
Actions
Public operations.
getOrDerive: The callback writes ordinary files below an empty directory.initopen
Fields and members
Public fields and members.
Source
Source: lib/cook/src/cache.zig:106
zig
pub const Store = struct { gpa: Allocator, io: std.Io, path: []const u8, limits: Limits, pub fn init(gpa: Allocator, io: std.Io, path: []const u8, limits: Limits) !Store { if (!std.fs.path.isAbsolute(path)) return error.CachePathNotAbsolute; if (limits.max_entry_bytes == 0 or limits.max_cache_bytes < limits.max_entry_bytes or limits.max_entries == 0 or limits.max_files == 0 or limits.max_directories == 0 or limits.max_depth == 0 or limits.max_path_bytes < 64) return error.InvalidLimits; return .{ .gpa = gpa, .io = io, .path = path, .limits = limits }; } /// The callback writes ordinary files below an empty directory. The /// returned entry holds its manifest lease until deinit. pub fn getOrDerive( store: Store, key: Key, context: anytype, comptime derive: fn (@TypeOf(context), std.Io, std.Io.Dir) anyerror!void, ) !Entry { try std.Io.Dir.cwd().createDirPath(store.io, store.path); const hex = key.hex(); const prefix = try std.fs.path.join(store.gpa, &.{ store.path, "v2", hex[0..2] }); defer store.gpa.free(prefix); try std.Io.Dir.cwd().createDirPath(store.io, prefix); var key_lock = try store.keyLock(prefix, key); defer key_lock.close(store.io); try key_lock.lock(store.io, .shared); const path = try std.fmt.allocPrint(store.gpa, "{s}/{s}", .{ prefix, hex[2..] }); errdefer store.gpa.free(path); if (try store.exists(path)) return store.load(&key_lock, key, path, true, null); key_lock.unlock(store.io); try key_lock.lock(store.io, .exclusive); if (try store.exists(path)) return store.load(&key_lock, key, path, true, null); const summary = try store.publish(prefix, path, key, context, derive); return store.load(&key_lock, key, path, false, summary); } pub fn open(store: Store, key: Key) !Entry { const hex = key.hex(); const prefix = try std.fs.path.join(store.gpa, &.{ store.path, "v2", hex[0..2] }); defer store.gpa.free(prefix); if (!try store.exists(prefix)) return error.CacheMiss; const path = try std.fmt.allocPrint(store.gpa, "{s}/v2/{s}/{s}", .{ store.path, hex[0..2], hex[2..] }); errdefer store.gpa.free(path); if (!try store.exists(path)) return error.CacheMiss; var key_lock = try store.keyLock(prefix, key); defer key_lock.close(store.io); try key_lock.lock(store.io, .shared); if (!try store.exists(path)) return error.CacheMiss; return store.load(&key_lock, key, path, true, null); } fn keyLock(store: Store, prefix: []const u8, key: Key) !std.Io.File { const hex = key.hex(); const lock_path = try std.fmt.allocPrint(store.gpa, "{s}/.key-{s}", .{ prefix, &hex }); defer store.gpa.free(lock_path); return std.Io.Dir.createFileAbsolute(store.io, lock_path, .{ .truncate = false, .read = true, .permissions = .fromMode(0o600), }); } fn load(store: Store, key_lock: *std.Io.File, key: Key, path: []u8, hit: bool, published: ?Summary) !Entry { var dir = try std.Io.Dir.openDirAbsolute(store.io, path, .{ .iterate = true, .follow_symlinks = false }); errdefer dir.close(store.io); var manifest = dir.openFile(store.io, ".cook", .{ .mode = .read_write }) catch return error.CorruptEntry; errdefer manifest.close(store.io); try manifest.lock(store.io, .shared); key_lock.unlock(store.io); const summary = published orelse try store.verify(dir, key); try store.touch(manifest); return .{ .io = store.io, .gpa = store.gpa, .path = path, .lock = manifest, .dir = dir, .summary = summary, .hit = hit }; } fn exists(store: Store, path: []const u8) !bool { _ = std.Io.Dir.cwd().statFile(store.io, path, .{ .follow_symlinks = false }) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; return true; } fn publish( store: Store, prefix: []const u8, path: []const u8, key: Key, context: anytype, comptime derive: fn (@TypeOf(context), std.Io, std.Io.Dir) anyerror!void, ) !Summary { const hex = key.hex(); const stage_path = try std.fmt.allocPrint(store.gpa, "{s}/.stage-{s}", .{ prefix, &hex }); defer store.gpa.free(stage_path); try std.Io.Dir.cwd().deleteTree(store.io, stage_path); try std.Io.Dir.cwd().createDir(store.io, stage_path, .default_dir); errdefer std.Io.Dir.cwd().deleteTree(store.io, stage_path) catch {}; var stage = try std.Io.Dir.openDirAbsolute(store.io, stage_path, .{ .iterate = true, .follow_symlinks = false }); defer stage.close(store.io); try derive(context, store.io, stage); const tree = try store.snapshot(stage, true); const encoded = encodeManifest(key, tree); var manifest = try stage.createFile(store.io, ".cook", .{ .exclusive = true, .permissions = .fromMode(0o600) }); defer manifest.close(store.io); try manifest.writePositionalAll(store.io, &encoded, 0); try manifest.sync(store.io); try syncDirectory(store.io, stage); const lock_path = try std.fs.path.join(store.gpa, &.{ store.path, ".lock" }); defer store.gpa.free(lock_path); var root_lock = try std.Io.Dir.createFileAbsolute(store.io, lock_path, .{ .truncate = false, .read = true, .permissions = .fromMode(0o600), }); defer root_lock.close(store.io); try root_lock.lock(store.io, .exclusive); try store.prune(tree.summary); var parent = try std.Io.Dir.openDirAbsolute(store.io, prefix, .{}); defer parent.close(store.io); try std.Io.Dir.cwd().rename(stage_path, std.Io.Dir.cwd(), path, store.io); try syncDirectory(store.io, parent); return tree.summary; } fn verify(store: Store, dir: std.Io.Dir, key: Key) !Summary { const encoded = dir.readFileAlloc(store.io, ".cook", store.gpa, .limited(85)) catch return error.CorruptEntry; defer store.gpa.free(encoded); if (encoded.len != 84 or !std.mem.eql(u8, encoded[0..8], "TCOOK001") or !std.mem.eql(u8, encoded[8..40], &key.digest)) return error.CorruptEntry; const tree = try store.snapshot(dir, false); const expected = encodeManifest(key, tree); if (!std.mem.eql(u8, encoded, &expected)) return error.CorruptEntry; return tree.summary; } fn touch(store: Store, manifest: std.Io.File) !void { try manifest.writePositionalAll(store.io, "T", 0); try manifest.sync(store.io); } fn prune(store: Store, pending: Summary) !void { const version = try std.fs.path.join(store.gpa, &.{ store.path, "v2" }); defer store.gpa.free(version); var root = try std.Io.Dir.openDirAbsolute(store.io, version, .{ .iterate = true, .follow_symlinks = false }); defer root.close(store.io); var stages: std.ArrayList(Candidate) = .empty; defer { for (stages.items) |item| store.gpa.free(item.path); stages.deinit(store.gpa); } var candidates: std.ArrayList(Candidate) = .empty; defer { for (candidates.items) |item| store.gpa.free(item.path); candidates.deinit(store.gpa); } var total_bytes: u64 = 0; var prefixes = root.iterate(); while (try prefixes.next(store.io)) |prefix| { if (prefix.kind != .directory or prefix.name.len != 2) continue; const prefix_path = try std.fs.path.join(store.gpa, &.{ version, prefix.name }); defer store.gpa.free(prefix_path); var shard = try root.openDir(store.io, prefix.name, .{ .iterate = true, .follow_symlinks = false }); defer shard.close(store.io); var children = shard.iterate(); while (try children.next(store.io)) |child| { if (child.kind != .directory) continue; const path = try std.fs.path.join(store.gpa, &.{ prefix_path, child.name }); errdefer store.gpa.free(path); if (std.mem.startsWith(u8, child.name, ".stage-")) { if (child.name.len != 71) { store.gpa.free(path); continue; } if (stages.items.len == 65536) return error.CacheInventoryExceeded; try stages.append(store.gpa, .{ .path = path, .key = Key.parseHex(child.name[7..]) catch return error.CacheInventoryExceeded, .bytes = 0, .mtime = 0, }); continue; } if (child.name.len != 62) { store.gpa.free(path); continue; } if (candidates.items.len == 65536) return error.CacheInventoryExceeded; var hex: [64]u8 = undefined; @memcpy(hex[0..2], prefix.name); @memcpy(hex[2..], child.name); const key = Key.parseHex(&hex) catch return error.CacheInventoryExceeded; var entry_dir = try shard.openDir(store.io, child.name, .{ .follow_symlinks = false }); defer entry_dir.close(store.io); const stat = try entry_dir.statFile(store.io, ".cook", .{ .follow_symlinks = false }); const encoded = try entry_dir.readFileAlloc(store.io, ".cook", store.gpa, .limited(85)); defer store.gpa.free(encoded); const bytes = if (encoded.len == 84 and std.mem.eql(u8, encoded[0..8], "TCOOK001")) std.mem.readInt(u64, encoded[44..52], .little) else store.limits.max_entry_bytes; total_bytes = std.math.add(u64, total_bytes, bytes) catch return error.CacheInventoryExceeded; try candidates.append(store.gpa, .{ .path = path, .key = key, .bytes = bytes, .mtime = @intCast(stat.mtime.nanoseconds), }); } } for (stages.items) |stage| { const parent_path = std.fs.path.dirname(stage.path).?; var key_lock = try store.keyLock(parent_path, stage.key); defer key_lock.close(store.io); if (!try key_lock.tryLock(store.io, .exclusive)) continue; try std.Io.Dir.cwd().deleteTree(store.io, stage.path); var parent = try std.Io.Dir.openDirAbsolute(store.io, parent_path, .{}); defer parent.close(store.io); try syncDirectory(store.io, parent); } std.mem.sortUnstable(Candidate, candidates.items, {}, candidateLessThan); var count: usize = candidates.items.len; total_bytes = std.math.add(u64, total_bytes, pending.bytes) catch return error.CacheInventoryExceeded; count += 1; for (candidates.items) |candidate| { if (count <= store.limits.max_entries and total_bytes <= store.limits.max_cache_bytes) break; const parent_path = std.fs.path.dirname(candidate.path).?; var key_lock = try store.keyLock(parent_path, candidate.key); defer key_lock.close(store.io); if (!try key_lock.tryLock(store.io, .exclusive)) continue; const manifest_path = try std.fs.path.join(store.gpa, &.{ candidate.path, ".cook" }); defer store.gpa.free(manifest_path); var manifest = try std.Io.Dir.openFileAbsolute(store.io, manifest_path, .{ .mode = .read_write }); defer manifest.close(store.io); if (!try manifest.tryLock(store.io, .exclusive)) continue; try std.Io.Dir.cwd().deleteTree(store.io, candidate.path); var parent = try std.Io.Dir.openDirAbsolute(store.io, parent_path, .{}); defer parent.close(store.io); try syncDirectory(store.io, parent); total_bytes -= candidate.bytes; count -= 1; } if (count > store.limits.max_entries or total_bytes > store.limits.max_cache_bytes) return error.CacheFull; } fn snapshot(store: Store, dir: std.Io.Dir, sync_files: bool) !Snapshot { var records: std.ArrayList(Record) = .empty; defer { for (records.items) |record| store.gpa.free(record.path); records.deinit(store.gpa); } var summary: Summary = .{ .files = 0, .bytes = 0 }; var directories: u32 = 0; try store.walk(dir, "", 0, sync_files, &records, &summary, &directories); std.mem.sortUnstable(Record, records.items, {}, recordLessThan); var hasher = Sha256.init(.{}); for (records.items) |record| { hashFramed(&hasher, record.path); hasher.update(&.{record.kind}); var size: [8]u8 = undefined; std.mem.writeInt(u64, &size, record.size, .little); hasher.update(&size); hasher.update(&record.digest); } var digest: [32]u8 = undefined; hasher.final(&digest); return .{ .summary = summary, .digest = digest }; } fn walk( store: Store, dir: std.Io.Dir, relative: []const u8, depth: u8, sync_files: bool, records: *std.ArrayList(Record), summary: *Summary, directories: *u32, ) !void { var iterator = dir.iterate(); while (try iterator.next(store.io)) |item| { if (depth == 0 and std.mem.eql(u8, item.name, ".cook")) { if (sync_files) return error.ReservedPath; continue; } const child_path = if (relative.len == 0) try store.gpa.dupe(u8, item.name) else try std.fs.path.join(store.gpa, &.{ relative, item.name }); errdefer store.gpa.free(child_path); if (child_path.len > store.limits.max_path_bytes) return error.PathLimitExceeded; switch (item.kind) { .directory => { if (depth == store.limits.max_depth) return error.DepthLimitExceeded; if (directories.* == store.limits.max_directories) return error.DirectoryLimitExceeded; directories.* += 1; var child = try dir.openDir(store.io, item.name, .{ .iterate = true, .follow_symlinks = false }); defer child.close(store.io); try store.walk(child, child_path, depth + 1, sync_files, records, summary, directories); if (sync_files) try syncDirectory(store.io, child); try records.append(store.gpa, .{ .path = child_path, .kind = 0, .size = 0, .digest = @splat(0) }); }, .file => { if (summary.files == store.limits.max_files) return error.FileLimitExceeded; var file = try dir.openFile(store.io, item.name, .{}); defer file.close(store.io); const stat = try file.stat(store.io); if (stat.size > store.limits.max_entry_bytes -| summary.bytes) return error.EntrySizeExceeded; const digest = try digestFile(store.io, file); if (sync_files) try file.sync(store.io); try records.append(store.gpa, .{ .path = child_path, .kind = 1, .size = stat.size, .digest = digest }); summary.files += 1; summary.bytes += stat.size; }, else => return error.UnsupportedFileType, } } }};Source: lib/cook/src/root.zig:5
zig
pub const Store = cache.Store;Audit
| Definitions | 4 |
|---|---|
| Public names | 4 |
| Members | 4 |
| Version | 26.7.0 |
| Revision | daab053ee433 |