repair.v

6.1 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		seen[id] = true27		object := backend.get(save_key(repo, id))!28		if !object.found {29			return error('the remote does not have save ${id} either')30		}31		record := decode_save(object.body)!32		if record.id != id {33			return error('remote save ${id} holds the record of ${record.id}')34		}35		// The same scalar rules Doctor checks locally: an invalid record is36		// refused here, not imported for Doctor to report afterwards.37		if record.created_at < 0 || record.size_bytes < 0 {38			return error('remote save ${id} has invalid metadata')39		}40		for file in record.files {41			if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects42				&& !strips.any(reaches(it, id) && covers(it.path, file.path)) {43				blob := backend.get(object_key(repo, file.hash))!44				if !blob.found {45					return error('the remote is missing object ${file.hash} that save ${id} needs')46				}47				fetched.repo_objects[file.hash] = blob.body48			}49		}50		fetched.saves << record51		wanted << record.parent52		wanted << record.restores53	}54	// The saves table refers to each parent, so parents land first.55	mut ordered := []SaveRecord{}56	mut placed := map[string]bool{}57	for ordered.len < fetched.saves.len {58		before := ordered.len59		for record in fetched.saves {60			if record.id !in placed && (record.parent !in seen || record.parent in placed) {61				ordered << record62				placed[record.id] = true63			}64		}65		if ordered.len == before {66			return error('the remote history has a parent cycle')67		}68	}69	fetched.saves = ordered70	store.begin()!71	write_fetched_records(store, fetched) or {72		store.rollback()73		return err74	}75	store.end()!76	return ordered.len77}7879/// Upload verified local copies of remote objects that are missing or damaged,80/// and of missing save records with their objects.81pub fn resend(store &storage.Store, mut backend remote.Backend, repo string, hashes []string, save_ids []string) !int {82	// A record sent before this repo applies a remote strip would bring the83	// stripped path back.84	if unapplied_strips(store, mut backend, repo)!.len > 0 {85		return error(strip_not_applied)86	}87	mut uploaded := Uploaded{}88	upload_copies(store, mut backend, repo, hashes, save_ids, mut uploaded) or {89		return partway(uploaded.done(), after_failed_upload(store, mut backend, repo, uploaded,90			err))91	}92	// As in offer: a strip that landed during the uploads may have missed them.93	if undo_late_strips(store, mut backend, repo, uploaded) or {94		return partway(uploaded.done(), err)95	} {96		return partway(uploaded.done(), error(strip_not_applied))97	}98	return uploaded.objects.len + uploaded.saves.len99}100101fn upload_copies(store &storage.Store, mut backend remote.Backend, repo string, hashes []string, save_ids []string, mut uploaded Uploaded) ! {102	for hash in hashes {103		key := object_key(repo, hash)104		existing := backend.get(key)!105		if existing.found {106			// Another machine may have put it right since the audit. Bytes that do107			// not decompress are damaged, even where empty bytes would hash right.108			if data := zlib.decompress(existing.body) {109				if storage.hash_hex(data) == hash {110					continue111				}112			}113			backend.delete(key)!114			uploaded.deleted++115		}116		store.get_object(hash)!117		if backend.put_if_absent(key, store.object_blob(hash)!)! {118			uploaded.objects[hash] = true119		}120	}121	for id in save_ids {122		save := history.by_id(store, id) or { return error('save ${id} is missing here too') }123		for file in history.files_of(store, id)! {124			store.get_object(file.hash)!125			if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {126				uploaded.objects[file.hash] = true127			}128		}129		if backend.put_if_absent(save_key(repo, id), encode_save(save_record(store, save)!).bytes())! {130			uploaded.saves << id131		}132	}133}134135/// Delete what an upload sent before a late strip it could not settle: each136/// object no remote record lists that this repo, with every remote strip137/// applied, neither holds nor names in a save.138pub fn settle_unsettled(store &storage.Store, mut backend remote.Backend, repo string) ! {139	if unapplied_strips(store, mut backend, repo)!.len > 0 {140		return error(strip_not_applied)141	}142	mut needed := map[string]bool{}143	for key in backend.list('${repo}/saves')! {144		object := backend.get(key)!145		if object.found {146			for file in decode_save(object.body)!.files {147				needed[file.hash] = true148			}149		}150	}151	mut deleted := 0152	for hash in unsettled(store)! {153		if hash !in needed && storage.valid_hash(hash) && !store.has_object(hash)!154			&& store.paths_of_object(hash)!.len == 0 {155			backend.delete(object_key(repo, hash)) or {156				return partway(done_text('deleted', deleted, 'remote object'), err)157			}158			deleted++159		}160	}161	store.remove_state(unsettled_key) or {162		return partway(done_text('deleted', deleted, 'remote object'), err)163	}164}165166/// Remove what recorded strips cover but a save still holds, without167/// recording a new strip.168pub fn reapply_strips(store &storage.Store) !int {169	store.begin()!170	removed := reapply_valid_strips(store) or {171		store.rollback()172		return err173	}174	store.end()!175	return removed176}177178// An invalid strip event is reported, never applied, and only the objects a179// strip frees go with it.180fn reapply_valid_strips(store &storage.Store) !int {181	mut removed := 0182	for event in events.strip_events(store)!.filter(events.valid_strip(it)) {183		hashes := stripped_hashes(store, event.path, event.save_id)!184		removed += remove_path(store, event.path, event.save_id)!185		store.drop_unused(hashes)!186	}187	return removed188}