12345678module snapshot910import os11import src.storage1213141516const restoring_key = 'restoring'1718pub const restore_unfinished = 'an earlier restore stopped before all its files were written — run koh revert to finish it (it records the tree as it is in a checkpoint first)'1920pub fn restore_pending(store &storage.Store) bool {21 return store.state(restoring_key) != none22}2324pub fn clear_restore_mark(store &storage.Store) ! {25 store.remove_state(restoring_key)!26}2728pub struct RestoreReport {29pub:30 written int31 removed int32}3334353637383940pub fn restore_files(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {41 42 marks := root == store.root43 if marks {44 store.set_state(restoring_key, 'yes')!45 }46 mut wanted := map[string]bool{}47 for entry in target {48 wanted[entry.path] = true49 }50 mut removed := 051 for path in remove {52 full := os.join_path(root, path)53 if !os.is_file(full) || ig.matches(path, false) || path in wanted {54 continue55 }56 os.rm(full)!57 removed++58 prune_empty_dirs(root, os.dir(full))59 }60 61 62 63 for entry in target {64 full := os.join_path(root, entry.path)65 if os.is_dir(full) {66 os.rmdir(full) or {67 return error('${entry.path} is a folder of ignored files where a saved file belongs — move it, then run the command again')68 }69 }70 }71 mut written := 072 cache := store.load_cache()!73 for entry in target {74 if ig.matches(entry.path, false) && os.exists(os.join_path(root, entry.path)) {75 continue76 }77 78 79 if already_there(root, entry, cache) {80 continue81 }82 data := store.get_object(entry.hash)!83 write_file(os.join_path(root, entry.path), data, entry.executable)!84 written++85 }86 if marks {87 clear_restore_mark(store)!88 }89 return RestoreReport{90 written: written91 removed: removed92 }93}9495969798fn already_there(root string, entry FileEntry, cache map[string]storage.CacheRow) bool {99 full := os.join_path(root, entry.path)100 st := lstat(full) or { return false }101 if st.kind != .file || st.size != entry.size || is_executable(st.mode) != entry.executable {102 return false103 }104 if row := cache[entry.path] {105 if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {106 return row.hash == entry.hash107 }108 }109 data := os.read_bytes(full) or { return false }110 return storage.hash_hex(data) == entry.hash111}112113114115116117pub fn check_links(root string, files []FileEntry) ! {118 for file in files {119 mut folder := ''120 for part in file.path.split('/')#[..-1] {121 folder = if folder == '' { part } else { '${folder}/${part}' }122 st := lstat(os.join_path(root, folder)) or { break }123 if st.kind == .symlink {124 return error('${folder} is a symbolic link where a saved folder belongs — koh will not write through it; move it, then run the command again')125 }126 }127 }128}129130fn write_file(path string, data []u8, executable bool) ! {131 os.mkdir_all(os.dir(path), os.MkdirParams{})!132 tmp := '${path}.koh-tmp-${os.getpid()}'133 os.write_file_array(tmp, data)!134 mode := if executable { 0o755 } else { 0o644 }135 os.chmod(tmp, mode)!136 os.rename(tmp, path)!137}138139140fn prune_empty_dirs(root string, dir string) {141 mut current := dir142 for current.len > root.len && current.starts_with(root) {143 if (os.ls(current) or { return }).len > 0 {144 return145 }146 os.rmdir(current) or { return }147 current = os.dir(current)148 }149}