restore.v

2.5 KB · raw

1//! Writing a tree back onto disk.2//!3//! A restore writes exactly the target's files and deletes the tracked files4//! the target does not have. Ignored paths are never touched, and every file5//! lands through a temp file and a rename, so an interrupted restore leaves a6//! readable tree rather than half-written files.78module snapshot910import os11import src.storage1213pub struct RestoreReport {14pub:15	written int16	removed int17}1819// restore_files writes exactly `target` into the working tree and deletes the20// paths in `remove` (tracked files the target does not have). Ignored paths are21// never touched — not written, not deleted — even if a caller asks. Every file22// lands through a temp file and a rename, so an interrupted restore leaves a23// readable tree rather than half-written files.24pub fn restore_files(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {25	mut wanted := map[string]bool{}26	for entry in target {27		wanted[entry.path] = true28	}29	mut removed := 030	for path in remove {31		full := os.join_path(root, path)32		if !os.is_file(full) || ig.matches(path, false) || path in wanted {33			continue34		}35		os.rm(full)!36		removed++37		prune_empty_dirs(root, os.dir(full))38	}39	// A folder where a file belongs: gone if the removals emptied it, else it40	// holds ignored files, which are never touched. Refuse before writing, as41	// V's move would otherwise drop the file inside the folder.42	for entry in target {43		full := os.join_path(root, entry.path)44		if os.is_dir(full) {45			os.rmdir(full) or {46				return error('${entry.path} is a folder of ignored files where a saved file belongs — move it, then run the command again')47			}48		}49	}50	for entry in target {51		data := store.get_object(entry.hash)!52		write_file(os.join_path(root, entry.path), data, entry.executable)!53	}54	return RestoreReport{55		written: target.len56		removed: removed57	}58}5960fn write_file(path string, data []u8, executable bool) ! {61	os.mkdir_all(os.dir(path), os.MkdirParams{})!62	tmp := '${path}.koh-tmp-${os.getpid()}'63	os.write_file_array(tmp, data)!64	mode := if executable { 0o755 } else { 0o644 }65	os.chmod(tmp, mode)!66	os.rename(tmp, path)!67}6869// prune_empty_dirs clears directories a restore emptied, never the repo root.70fn prune_empty_dirs(root string, dir string) {71	mut current := dir72	for current.len > root.len && current.starts_with(root) {73		if (os.ls(current) or { return }).len > 0 {74			return75		}76		os.rmdir(current) or { return }77		current = os.dir(current)78	}79}