doctor_repair.v

8.4 KB · raw

1//! Doctor's repairs (spec §7.6.2). Each one puts back verified data or2//! finishes interrupted work; none changes what a save contains.34module cli56import compress.zlib7import os8import src.exchange9import src.fmt10import src.history11import src.machine12import src.snapshot13import src.storage1415struct DoctorRepair {16	action string17	run    fn () ! @[required]18}1920fn plan_repairs(store &storage.Store, report DoctorJson) []DoctorRepair {21	mut repairs := []DoctorRepair{}22	has_remote := report.source != '' || machine.has_remote()23	// A source is read-only, so only this machine's own remote takes uploads.24	writable_remote := report.source == '' && machine.has_remote()25	source, remote_repo := report.source, report.remote_repo26	if report.checks.any(it.name == 'SQLite integrity' && it.status == 'fail') {27		repairs << DoctorRepair{'rebuild the database indexes', fn [store] () ! {28			store.db.exec('REINDEX')!29		}}30	}31	if report.damaged_objects.len > 0 {32		hashes := report.damaged_objects33		repairs << DoctorRepair{'restore ${fmt.plural(hashes.len, 'damaged or missing object')} from verified copies', fn [store, hashes, source, remote_repo] () ! {34			repair_objects(store, hashes, source, remote_repo)!35		}}36	}37	if report.missing_saves.len > 0 && has_remote {38		ids := report.missing_saves39		repairs << DoctorRepair{'fetch ${fmt.plural(ids.len, 'missing save')} from the remote', fn [store, ids, source, remote_repo] () ! {40			mut backend, repo := doctor_backend(store, source, remote_repo)!41			exchange.fetch_missing(store, mut backend, repo, ids)!42		}}43	} else if report.metadata_available && report.saves > 0 && report.head.id == '' {44		// A remote may hold a newer head; only a full, stable audit that found45		// none other there lets Doctor guess, and the remote is asked again.46		audited := !report.checks.any(it.name in ['remote audit', 'remote stability']47			&& it.status != 'pass')48		tip := only_tip(store) or { '' }49		seen := report.head_id50		if tip != '' && (!has_remote || (audited && report.remote_head in ['', tip])) {51			repairs << DoctorRepair{'point head at [${tip}], the only save nothing follows', fn [store, tip, seen, has_remote, source, remote_repo] () ! {52				if has_remote {53					mut backend, repo := doctor_backend(store, source, remote_repo)!54					theirs := backend.get(exchange.head_key(repo))!55					if theirs.found && theirs.body.bytestr().trim_space() != tip {56						return error('the remote now has another head; run koh doctor again')57					}58				}59				store.begin()!60				set_only_tip(store, tip, seen) or {61					store.rollback()62					return err63				}64				store.end()!65			}}66		}67	}68	if report.strip_residue {69		repairs << DoctorRepair{'remove stripped paths that saves still hold', fn [store] () ! {70			exchange.reapply_strips(store)!71		}}72	}73	if report.restore_unfinished {74		repairs << DoctorRepair{'finish the interrupted restore (koh revert keeps the current files in a checkpoint)', fn [store] () ! {75			finish_restore(store)!76		}}77	}78	if report.pending_strips.len > 0 && writable_remote {79		paths := report.pending_strips80		repairs << DoctorRepair{'send ${fmt.plural(paths.len, 'unfinished strip')} to the remote', fn [store, paths, remote_repo] () ! {81			send_strips(store, remote_repo, paths, false)!82		}}83	}84	// A strip whose remote half failed after the note was gone, as when an85	// upload raced it, is sent again from the recorded event.86	if report.remote_strip_residue.len > 0 && writable_remote {87		paths := report.remote_strip_residue88		repairs << DoctorRepair{'remove ${fmt.plural(paths.len, 'stripped path')} the remote still holds', fn [store, paths, remote_repo] () ! {89			send_strips(store, remote_repo, paths, true)!90		}}91	}92	if report.upload_unsettled && writable_remote {93		repairs << DoctorRepair{'delete what an interrupted upload sent that a strip removed', fn [store, remote_repo] () ! {94			mut backend, repo := doctor_backend(store, '', remote_repo)!95			exchange.settle_unsettled(store, mut backend, repo)!96		}}97	}98	// With damaged rows an unused object may be the only copy of saved content.99	if report.unreferenced_objects > 0 && report.problems.len == 0 {100		repairs << DoctorRepair{'delete ${fmt.plural(int(report.unreferenced_objects), 'object')} no save uses', fn [store] () ! {101			store.begin()!102			store.drop_unreferenced_objects() or {103				store.rollback()104				return err105			}106			store.end()!107		}}108	}109	if writable_remote {110		hashes := report.remote_bad_objects.filter(stored_intact(store, it))111		ids := report.remote_missing_saves.filter(history.by_id(store, it) != none)112		if hashes.len + ids.len > 0 {113			repairs << DoctorRepair{'upload ${fmt.plural(hashes.len, 'object')} and ${fmt.plural(ids.len, 'save')} the remote lacks or holds damaged', fn [store, hashes, ids, remote_repo] () ! {114				mut backend, repo := doctor_backend(store, '', remote_repo)!115				exchange.resend(store, mut backend, repo, hashes, ids)!116			}}117		}118	}119	return repairs120}121122// Each copy must hash back to its key before it replaces anything: first the123// stored bytes (only the size was wrong), then working files, then the remote.124fn repair_objects(store &storage.Store, hashes []string, source string, remote_repo string) ! {125	mut lost := []string{}126	for index, hash in hashes {127		restored := restore_object(store, hash, source, remote_repo) or {128			return exchange.partway(done_text('restored', index - lost.len, 'damaged or missing object'),129				err)130		}131		if !restored {132			lost << hash133		}134	}135	if lost.len > 0 {136		failure := error('no verified copy here or on the remote of ${lost.join(', ')}')137		return exchange.partway(done_text('restored', hashes.len - lost.len, 'damaged or missing object'),138			failure)139	}140}141142fn send_strips(store &storage.Store, remote_repo string, paths []string, requeue bool) ! {143	mut backend, _ := doctor_backend(store, '', remote_repo)!144	for index, path in paths {145		if requeue {146			exchange.requeue_strip(store, path) or {147				return exchange.partway(done_text('sent', index, 'strip'), err)148			}149		}150		exchange.send_strip(store, mut backend, path) or {151			return exchange.partway(done_text('sent', index, 'strip'), err)152		}153	}154}155156// done_text is what a repair finished before it failed, if anything.157fn done_text(verb string, done int, noun string) []string {158	return if done == 0 { []string{} } else { ['${verb} ${fmt.plural(done, noun)}'] }159}160161fn restore_object(store &storage.Store, hash string, source string, remote_repo string) !bool {162	if data := local_copy(store, hash) {163		store.repair_object(hash, data)!164		return true165	}166	if source == '' && !machine.has_remote() {167		return false168	}169	mut backend, repo := doctor_backend(store, source, remote_repo)!170	blob := backend.get(exchange.object_key(repo, hash))!171	data := zlib.decompress(blob.body) or { return false }172	if !blob.found || storage.hash_hex(data) != hash {173		return false174	}175	store.repair_object(hash, data)!176	return true177}178179fn stored_intact(store &storage.Store, hash string) bool {180	store.get_object(hash) or { return false }181	return true182}183184fn local_copy(store &storage.Store, hash string) ?[]u8 {185	if data := store.get_object(hash) {186		return data187	}188	for path in store.paths_of_object(hash) or { return none } {189		if !snapshot.legal_path(path) {190			continue191		}192		data := os.read_bytes(os.join_path(store.root, path)) or { continue }193		if storage.hash_hex(data) == hash {194			return data195		}196	}197	return none198}199200// A head lost with no remote copy can only be guessed safely when exactly one201// save has no successor.202fn only_tip(store &storage.Store) ?string {203	saves := history.all_saves(store) or { return none }204	mut parents := map[string]bool{}205	for save in saves {206		parents[save.parent] = true207	}208	tips := saves.filter(it.id !in parents)209	return if tips.len == 1 { tips[0].id } else { none }210}211212// Another koh command may have saved while Doctor asked; the tip seen then213// is the head only if nothing has changed since.214fn set_only_tip(store &storage.Store, tip string, seen string) ! {215	if (store.state(storage.head_key) or { '' }) != seen || (only_tip(store) or { '' }) != tip {216		return error('history changed since Doctor checked it; run koh doctor again')217	}218	history.set_head(store, tip)!219}220221// The same steps as koh revert, without its output, which would break --json.222fn finish_restore(store &storage.Store) ! {223	head := history.head(store) or { return error('this repo has no head to restore') }224	if !compare_with_head(store, head)!.dirty {225		snapshot.clear_restore_mark(store)!226		return227	}228	plan := plan_restore(store, head)!229	apply_restore(store, head, 'revert', plan, true)!230	snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!231}