restore.v

5.0 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.storage1213// Set while a restore writes into the repo. A restore records its save first,14// so a mark still here means the files stopped part way: the tree is half one15// save and half another, and nobody's work.16const 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}3334// restore_files writes `target` into the working tree and deletes the paths in35// `remove` (tracked files the target does not have). An ignored file already36// on disk is left as it is, even when the target saved an older copy: it may37// hold work no save has. A missing one is written. Every file lands through a38// temp file and a rename, so an interrupted restore leaves a readable tree39// rather than half-written files.40pub fn restore_files(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {41	// Peek writes elsewhere; only the repo's own tree is marked.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	// A folder where a file belongs: gone if the removals emptied it, else it61	// holds ignored files, which are never touched. Refuse before writing, as62	// V's move would otherwise drop the file inside the folder.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		// Rewriting a file that already matches would only reset its mtime,78		// and make the next status hash it again.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}9495// already_there reports whether the file on disk has the target's content and96// executable bit. The file cache answers without reading when the file is as97// it was when last hashed.98fn 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}112113// check_links refuses a restore that would write through a symlinked folder.114// Koh never follows links, so the folder it points at is not the repo's, and115// writing there would overwrite files outside it. Callers run it before they116// record anything.117pub 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}138139// prune_empty_dirs clears directories a restore emptied, never the repo root.140fn 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}