repair.v

5.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	strips := known_strips(store, fetched)!19	mut wanted := ids.clone()20	mut seen := map[string]bool{}21	for wanted.len > 0 {22		id := wanted.pop()23		if id == '' || id in seen || history.by_id(store, id) != none {24			continue25		}26		if !valid_save_id(id) {27			return error('remote history names invalid save ID ${id}')28		}29		seen[id] = true30		object := backend.get(save_key(repo, id))!31		if !object.found {32			return error('the remote does not have save ${id} either')33		}34		mut record := decode_save(object.body)!35		validate_save(record, id)!36		drop_unsafe_paths(mut record)37		validate_hashes(record.id, record.files)!38		for file in record.files {39			if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects40				&& !strips.any(reaches(it, id) && covers(it.path, file.path)) {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 partway(uploaded.done(), 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			uploaded.deleted++113		}114		store.get_object(hash)!115		if backend.put_if_absent(key, store.object_blob(hash)!)! {116			uploaded.objects[hash] = true117		}118	}119	for id in save_ids {120		save := history.by_id(store, id) or { return error('save ${id} is missing here too') }121		for file in history.files_of(store, id)! {122			store.get_object(file.hash)!123			if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {124				uploaded.objects[file.hash] = true125			}126		}127		if backend.put_if_absent(save_key(repo, id), encode_save(save_record(store, save)!).bytes())! {128			uploaded.saves << id129		}130	}131}132133/// Delete what an upload sent before a late strip it could not settle: each134/// object no remote record lists that this repo, with every remote strip135/// applied, neither holds nor names in a save.136pub fn settle_unsettled(store &storage.Store, mut backend remote.Backend, repo string) ! {137	if unapplied_strips(store, mut backend, repo)!.len > 0 {138		return error(strip_not_applied)139	}140	mut needed := map[string]bool{}141	for key in backend.list('${repo}/saves')! {142		object := backend.get(key)!143		if object.found {144			for file in decode_save(object.body)!.files {145				needed[file.hash] = true146			}147		}148	}149	mut deleted := 0150	for hash in unsettled(store)! {151		if hash !in needed && storage.valid_hash(hash) && !store.has_object(hash)!152			&& store.paths_of_object(hash)!.len == 0 {153			backend.delete(object_key(repo, hash)) or {154				return partway(done_text('deleted', deleted, 'remote object'), err)155			}156			deleted++157		}158	}159	store.remove_state(unsettled_key) or {160		return partway(done_text('deleted', deleted, 'remote object'), err)161	}162}163164/// Remove what recorded strips cover but a save still holds, without165/// recording a new strip.166pub fn reapply_strips(store &storage.Store) !int {167	store.begin()!168	removed := reapply_valid_strips(store) or {169		store.rollback()170		return err171	}172	store.end()!173	return removed174}175176// An invalid strip event is reported, never applied, and only the objects a177// strip frees go with it.178fn reapply_valid_strips(store &storage.Store) !int {179	mut removed := 0180	for event in events.strip_events(store)!.filter(events.valid_strip(it)) {181		hashes := stripped_hashes(store, event.path, event.save_id)!182		removed += remove_path(store, event.path, event.save_id)!183		store.drop_unused(hashes)!184	}185	return removed186}