repair.v

4.9 KB · raw

1//! Doctor's repairs that copy records between this repo and its remote.2//!3//! Each one only puts back what another copy of the repo verifiably holds;4//! none changes what a save contains.56module exchange78import compress.zlib9import src.events10import src.history11import src.remote12import src.storage1314/// Fetch saves this repo is missing, with any missing ancestors and objects,15/// in one transaction.16pub fn fetch_missing(store &storage.Store, mut backend remote.Backend, repo string, ids []string) !int {17	mut fetched := Fetched{}18	mut wanted := ids.clone()19	mut seen := map[string]bool{}20	for wanted.len > 0 {21		id := wanted.pop()22		if id == '' || id in seen || history.by_id(store, id) != none {23			continue24		}25		seen[id] = true26		object := backend.get(save_key(repo, id))!27		if !object.found {28			return error('the remote does not have save ${id} either')29		}30		record := decode_save(object.body)!31		if record.id != id {32			return error('remote save ${id} holds the record of ${record.id}')33		}34		// The same scalar rules Doctor checks locally: an invalid record is35		// refused here, not imported for Doctor to report afterwards.36		if record.created_at < 0 || record.size_bytes < 0 {37			return error('remote save ${id} has invalid metadata')38		}39		for file in record.files {40			if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {41				blob := backend.get(object_key(repo, file.hash))!42				if !blob.found {43					return error('the remote is missing object ${file.hash} that save ${id} needs')44				}45				fetched.repo_objects[file.hash] = blob.body46			}47		}48		fetched.saves << record49		wanted << record.parent50		wanted << record.restores51	}52	// The saves table refers to each parent, so parents land first.53	mut ordered := []SaveRecord{}54	mut placed := map[string]bool{}55	for ordered.len < fetched.saves.len {56		before := ordered.len57		for record in fetched.saves {58			if record.id !in placed && (record.parent !in seen || record.parent in placed) {59				ordered << record60				placed[record.id] = true61			}62		}63		if ordered.len == before {64			return error('the remote history has a parent cycle')65		}66	}67	fetched.saves = ordered68	store.begin()!69	write_fetched_records(store, fetched) or {70		store.rollback()71		return err72	}73	store.end()!74	return ordered.len75}7677/// Upload verified local copies of remote objects that are missing or damaged,78/// and of missing save records with their objects.79pub fn resend(store &storage.Store, mut backend remote.Backend, repo string, hashes []string, save_ids []string) !int {80	// A record sent before this repo applies a remote strip would bring the81	// stripped path back.82	if unapplied_strips(store, mut backend, repo)!.len > 0 {83		return error(strip_not_applied)84	}85	mut uploaded := Uploaded{}86	upload_copies(store, mut backend, repo, hashes, save_ids, mut uploaded) or {87		return partway(uploaded.done(), after_failed_upload(store, mut backend, repo, uploaded,88			err))89	}90	// As in offer: a strip that landed during the uploads may have missed them.91	if undo_late_strips(store, mut backend, repo, uploaded) or {92		return partway(uploaded.done(), err)93	} {94		return error(strip_not_applied)95	}96	return uploaded.objects.len + uploaded.saves.len97}9899fn upload_copies(store &storage.Store, mut backend remote.Backend, repo string, hashes []string, save_ids []string, mut uploaded Uploaded) ! {100	for hash in hashes {101		key := object_key(repo, hash)102		existing := backend.get(key)!103		if existing.found {104			// Another machine may have put it right since the audit. Bytes that do105			// not decompress are damaged, even where empty bytes would hash right.106			if data := zlib.decompress(existing.body) {107				if storage.hash_hex(data) == hash {108					continue109				}110			}111			backend.delete(key)!112		}113		store.get_object(hash)!114		if backend.put_if_absent(key, store.object_blob(hash)!)! {115			uploaded.objects[hash] = true116		}117	}118	for id in save_ids {119		save := history.by_id(store, id) or { return error('save ${id} is missing here too') }120		for file in history.files_of(store, id)! {121			store.get_object(file.hash)!122			if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {123				uploaded.objects[file.hash] = true124			}125		}126		if backend.put_if_absent(save_key(repo, id), encode_save(save_record(store, save)!).bytes())! {127			uploaded.saves << id128		}129	}130}131132/// Remove what recorded strips cover but a save still holds, without133/// recording a new strip.134pub fn reapply_strips(store &storage.Store) !int {135	store.begin()!136	removed := reapply_valid_strips(store) or {137		store.rollback()138		return err139	}140	store.end()!141	return removed142}143144// An invalid strip event is reported, never applied, and only the objects a145// strip frees go with it.146fn reapply_valid_strips(store &storage.Store) !int {147	mut removed := 0148	for event in events.strip_events(store)!.filter(events.valid_strip(it)) {149		hashes := stripped_hashes(store, event.path, event.save_id)!150		removed += remove_path(store, event.path, event.save_id)!151		store.drop_unused(hashes)!152	}153	return removed154}