restore.v

3.6 KB · raw

1//! Writing a tree back onto disk.2//!3//! A restore writes the target's files and deletes the tracked files the4//! target does not have. An ignored file already on disk is never overwritten5//! or deleted, and every file lands through a temp file and a rename, so an6//! interrupted restore leaves a readable tree rather than half-written files.78module snapshot910import os11import src.storage1213pub struct RestoreReport {14pub:15	written int16	removed int17}1819// restore_files writes `target` into the working tree and deletes the paths in20// `remove` (tracked files the target does not have). An ignored file already21// on disk is left as it is, even when the target saved an older copy: it may22// hold work no save has. A missing one is written. Every file lands through a23// temp file and a rename, so an interrupted restore leaves a readable tree24// rather than half-written files.25pub fn restore_files(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {26	mut wanted := map[string]bool{}27	for entry in target {28		wanted[entry.path] = true29	}30	mut removed := 031	for path in remove {32		full := os.join_path(root, path)33		if !os.is_file(full) || ig.matches(path, false) || path in wanted {34			continue35		}36		os.rm(full)!37		removed++38		prune_empty_dirs(root, os.dir(full))39	}40	// A folder where a file belongs: gone if the removals emptied it, else it41	// holds ignored files, which are never touched. Refuse before writing, as42	// V's move would otherwise drop the file inside the folder.43	for entry in target {44		full := os.join_path(root, entry.path)45		if os.is_dir(full) {46			os.rmdir(full) or {47				return error('${entry.path} is a folder of ignored files where a saved file belongs — move it, then run the command again')48			}49		}50	}51	mut written := 052	cache := store.load_cache()!53	for entry in target {54		if ig.matches(entry.path, false) && os.exists(os.join_path(root, entry.path)) {55			continue56		}57		// Rewriting a file that already matches would only reset its mtime,58		// and make the next status hash it again.59		if already_there(root, entry, cache) {60			continue61		}62		data := store.get_object(entry.hash)!63		write_file(os.join_path(root, entry.path), data, entry.executable)!64		written++65	}66	return RestoreReport{67		written: written68		removed: removed69	}70}7172// already_there reports whether the file on disk has the target's content and73// executable bit. The file cache answers without reading when the file is as74// it was when last hashed.75fn already_there(root string, entry FileEntry, cache map[string]storage.CacheRow) bool {76	full := os.join_path(root, entry.path)77	st := lstat(full) or { return false }78	if st.kind != .file || st.size != entry.size || is_executable(st.mode) != entry.executable {79		return false80	}81	if row := cache[entry.path] {82		if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {83			return row.hash == entry.hash84		}85	}86	data := os.read_bytes(full) or { return false }87	return storage.hash_hex(data) == entry.hash88}8990fn write_file(path string, data []u8, executable bool) ! {91	os.mkdir_all(os.dir(path), os.MkdirParams{})!92	tmp := '${path}.koh-tmp-${os.getpid()}'93	os.write_file_array(tmp, data)!94	mode := if executable { 0o755 } else { 0o644 }95	os.chmod(tmp, mode)!96	os.rename(tmp, path)!97}9899// prune_empty_dirs clears directories a restore emptied, never the repo root.100fn prune_empty_dirs(root string, dir string) {101	mut current := dir102	for current.len > root.len && current.starts_with(root) {103		if (os.ls(current) or { return }).len > 0 {104			return105		}106		os.rmdir(current) or { return }107		current = os.dir(current)108	}109}