doctor_repair.v

5.6 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		if tip := only_tip(store) {45			repairs << DoctorRepair{'point head at [${tip}], the only save nothing follows', fn [store, tip] () ! {46				history.set_head(store, tip)!47			}}48		}49	}50	if report.strip_residue {51		repairs << DoctorRepair{'remove stripped paths that saves still hold', fn [store] () ! {52			exchange.reapply_strips(store)!53		}}54	}55	if report.restore_unfinished {56		repairs << DoctorRepair{'finish the interrupted restore (koh revert keeps the current files in a checkpoint)', fn [store] () ! {57			finish_restore(store)!58		}}59	}60	if report.pending_strips.len > 0 && writable_remote {61		paths := report.pending_strips62		repairs << DoctorRepair{'send ${fmt.plural(paths.len, 'unfinished strip')} to the remote', fn [store, paths, remote_repo] () ! {63			mut backend, _ := doctor_backend(store, '', remote_repo)!64			for path in paths {65				exchange.send_strip(store, mut backend, path)!66			}67		}}68	}69	// With damaged rows an unused object may be the only copy of saved content.70	if report.unreferenced_objects > 0 && report.problems.len == 0 {71		repairs << DoctorRepair{'delete ${fmt.plural(int(report.unreferenced_objects), 'object')} no save uses', fn [store] () ! {72			store.begin()!73			store.drop_unreferenced_objects() or {74				store.rollback()75				return err76			}77			store.end()!78		}}79	}80	if writable_remote {81		hashes := report.remote_bad_objects.filter(stored_intact(store, it))82		ids := report.remote_missing_saves.filter(history.by_id(store, it) != none)83		if hashes.len + ids.len > 0 {84			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] () ! {85				mut backend, repo := doctor_backend(store, '', remote_repo)!86				exchange.resend(store, mut backend, repo, hashes, ids)!87			}}88		}89	}90	return repairs91}9293// Each copy must hash back to its key before it replaces anything: first the94// stored bytes (only the size was wrong), then working files, then the remote.95fn repair_objects(store &storage.Store, hashes []string, source string, remote_repo string) ! {96	mut lost := []string{}97	for hash in hashes {98		if data := local_copy(store, hash) {99			store.repair_object(hash, data)!100			continue101		}102		if source == '' && !machine.has_remote() {103			lost << hash104			continue105		}106		mut backend, repo := doctor_backend(store, source, remote_repo)!107		blob := backend.get(exchange.object_key(repo, hash))!108		data := zlib.decompress(blob.body) or { []u8{} }109		if !blob.found || storage.hash_hex(data) != hash {110			lost << hash111			continue112		}113		store.repair_object(hash, data)!114	}115	if lost.len > 0 {116		return error('no verified copy here or on the remote of ${lost.join(', ')}')117	}118}119120fn stored_intact(store &storage.Store, hash string) bool {121	store.get_object(hash) or { return false }122	return true123}124125fn local_copy(store &storage.Store, hash string) ?[]u8 {126	if data := store.get_object(hash) {127		return data128	}129	for path in store.paths_of_object(hash) or { return none } {130		if !snapshot.legal_path(path) {131			continue132		}133		data := os.read_bytes(os.join_path(store.root, path)) or { continue }134		if storage.hash_hex(data) == hash {135			return data136		}137	}138	return none139}140141// A head lost with no remote copy can only be guessed safely when exactly one142// save has no successor.143fn only_tip(store &storage.Store) ?string {144	saves := history.all_saves(store) or { return none }145	mut parents := map[string]bool{}146	for save in saves {147		parents[save.parent] = true148	}149	tips := saves.filter(it.id !in parents)150	return if tips.len == 1 { tips[0].id } else { none }151}152153// The same steps as koh revert, without its output, which would break --json.154fn finish_restore(store &storage.Store) ! {155	head := history.head(store) or { return error('this repo has no head to restore') }156	if !compare_with_head(store, head)!.dirty {157		snapshot.clear_restore_mark(store)!158		return159	}160	plan := plan_restore(store, head)!161	apply_restore(store, head, 'revert', plan, true)!162	snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!163}