123module snapshot45import os6import time7import src.storage8910const racy_window_ns = i64(3_000_000_000)111213pub struct FileEntry {14pub:15 path string16 hash string17 size i6418 executable bool19}20212223pub fn walk(store &storage.Store, root string, ig Ignore) ![]FileEntry {24 cache := store.load_cache()!25 mut entries := []FileEntry{}26 mut fresh := []storage.CacheRow{}27 walk_dir(root, '', ig.banning(store.stripped_paths()!), cache, mut entries, mut fresh)!28 29 30 store.save_cache_rows(fresh) or {}31 entries.sort(a.path < b.path)32 return entries33}3435363738pub fn walk_raw(root string, ig Ignore) ![]FileEntry {39 mut entries := []FileEntry{}40 mut unused := []storage.CacheRow{}41 walk_dir(root, '', ig, map[string]storage.CacheRow{}, mut entries, mut unused)!42 entries.sort(a.path < b.path)43 return entries44}4546fn walk_dir(root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry, mut fresh []storage.CacheRow) ! {47 dir := if rel == '' { root } else { os.join_path(root, rel) }48 for name in os.ls(dir)! {49 child := if rel == '' { name } else { '${rel}/${name}' }50 st := lstat(os.join_path(root, child)) or { continue }51 52 53 if ig.matches(child, st.kind == .dir) {54 continue55 }56 match st.kind {57 .dir {58 walk_dir(root, child, ig, cache, mut out, mut fresh)!59 }60 .file {61 if st.size > storage.max_file_size {62 return error('${child} is ${st.size} bytes; koh stores code and text, not files this large')63 }64 hash := hash_file(root, child, st, cache, mut fresh)!65 out << FileEntry{66 path: child67 hash: hash68 size: st.size69 executable: is_executable(st.mode)70 }71 }72 else {}73 }74 }75}7677fn hash_file(root string, rel string, st Stat, cache map[string]storage.CacheRow, mut fresh []storage.CacheRow) !string {78 if row := cache[rel] {79 if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {80 return row.hash81 }82 }83 hash := storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)84 if cacheable(st, time.now().unix_nano()) {85 fresh << storage.CacheRow{86 path: rel87 size: st.size88 mtime_ns: st.mtime_ns89 ctime_ns: st.ctime_ns90 hash: hash91 }92 }93 return hash94}9596979899100101fn cacheable(st Stat, now_ns i64) bool {102 changed := if st.ctime_ns > st.mtime_ns { st.ctime_ns } else { st.mtime_ns }103 return now_ns - changed >= racy_window_ns104}105106107108pub fn store_objects(store &storage.Store, root string, entries []FileEntry) ! {109 for entry in entries {110 if store.has_object(entry.hash)! {111 continue112 }113 data := os.read_bytes(os.join_path(root, entry.path))!114 stored := store.put_object(data)!115 if stored != entry.hash {116 return error('${entry.path} changed while saving')117 }118 }119}120121122pub fn same_tree(a []FileEntry, b []FileEntry) bool {123 if a.len != b.len {124 return false125 }126 for i in 0 .. a.len {127 if a[i].path != b[i].path || a[i].hash != b[i].hash || a[i].executable != b[i].executable {128 return false129 }130 }131 return true132}