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}23242526pub fn mark_restore(store &storage.Store) ! {27 store.set_state(restoring_key, 'yes')!28}2930pub fn clear_restore_mark(store &storage.Store) ! {31 store.remove_state(restoring_key)!32}3334pub struct RestoreReport {35pub:36 written int37 removed int38}3940414243444546pub fn restore_files(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {47 48 marks := root == store.root49 mut wanted := map[string]bool{}50 for entry in target {51 wanted[entry.path] = true52 }53 mut removed := 054 for path in remove {55 full := os.join_path(root, path)56 if !os.is_file(full) || ig.matches(path, false) || path in wanted {57 continue58 }59 os.rm(full)!60 removed++61 prune_empty_dirs(root, os.dir(full))62 }63 64 65 66 for entry in target {67 full := os.join_path(root, entry.path)68 if os.is_dir(full) {69 os.rmdir(full) or {70 return error('${entry.path} is a folder of ignored files where a saved file belongs — move it, then run the command again')71 }72 }73 }74 mut written := 075 cache := store.load_cache()!76 for entry in target {77 if ig.matches(entry.path, false) && os.exists(os.join_path(root, entry.path)) {78 continue79 }80 81 82 if already_there(root, entry, cache) {83 continue84 }85 data := store.get_object(entry.hash)!86 write_file(os.join_path(root, entry.path), data, entry.executable)!87 written++88 }89 if marks {90 clear_restore_mark(store)!91 }92 return RestoreReport{93 written: written94 removed: removed95 }96}979899100101fn already_there(root string, entry FileEntry, cache map[string]storage.CacheRow) bool {102 full := os.join_path(root, entry.path)103 st := lstat(full) or { return false }104 if st.kind != .file || st.size != entry.size || is_executable(st.mode) != entry.executable {105 return false106 }107 if row := cache[entry.path] {108 if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {109 return row.hash == entry.hash110 }111 }112 data := os.read_bytes(full) or { return false }113 return storage.hash_hex(data) == entry.hash114}115116117118119120pub fn check_links(root string, files []FileEntry) ! {121 for file in files {122 mut folder := ''123 for part in file.path.split('/')#[..-1] {124 folder = if folder == '' { part } else { '${folder}/${part}' }125 st := lstat(os.join_path(root, folder)) or { break }126 if st.kind == .symlink {127 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')128 }129 }130 }131}132133fn write_file(path string, data []u8, executable bool) ! {134 os.mkdir_all(os.dir(path), os.MkdirParams{})!135 tmp := '${path}.koh-tmp-${os.getpid()}'136 os.write_file_array(tmp, data)!137 mode := if executable { 0o755 } else { 0o644 }138 os.chmod(tmp, mode)!139 os.rename(tmp, path)!140}141142143fn prune_empty_dirs(root string, dir string) {144 mut current := dir145 for current.len > root.len && current.starts_with(root) {146 if (os.ls(current) or { return }).len > 0 {147 return148 }149 os.rmdir(current) or { return }150 current = os.dir(current)151 }152}