cmd_doctor.v

3.5 KB · raw

1//! `koh doctor` — integrity check and cleanup.2//!3//! One source of truth means there is nothing to rebuild; what is left to check4//! is that the file is sound and that every object a save names is really5//! there. Doctor also tidies the things that are safe to lose: the file cache6//! and old peek folders.78module cli910import json11import os12import src.history13import src.storage1415pub fn cmd_doctor(raw []string) int {16	args := parse('doctor', raw, Options{17		values: ['backup']18		flags:  ['json']19	}) or { return usage_error(err.msg()) }20	store := open_store() or { return refuse(args.json(), err.msg()) }21	defer {22		store.close()23	}24	mut report := DoctorJson{25		problems: []string{}26	}2728	report.problems << store.integrity_check() or { return refuse(args.json(), err.msg()) }29	report.problems << check_objects(store) or { return refuse(args.json(), err.msg()) }30	report.problems << check_save_files(store) or { return refuse(args.json(), err.msg()) }3132	report.objects = int(store.object_count() or { 0 })33	report.saves = int(history.save_count(store) or { 0 })34	report.checkpoints = int(history.checkpoint_count(store) or { 0 })35	report.cached_files = int(store.cache_count() or { 0 })36	report.peek_folders_removed = sweep_peek_folders()37	store.drop_cache() or { return refuse(args.json(), err.msg()) }3839	if path := args.value_or_none('backup') {40		if os.exists(path) {41			return refuse(args.json(), '${path} already exists')42		}43		store.backup(path) or { return refuse(args.json(), err.msg()) }44		report.backup = path45	}4647	if args.json() {48		emit_json(json.encode(report))49	} else {50		if report.problems.len == 0 {51			ok('${store.root} is sound')52		} else {53			for problem in report.problems {54				warn(problem)55			}56		}57		note('${report.objects} objects · ${report.saves} saves · ${report.checkpoints} checkpoints')58		if report.backup != '' {59			ok('backed up to ${report.backup}')60		}61	}62	if report.problems.len > 0 {63		return 164	}65	return 066}6768// check_objects reads every object and hashes it back. Nothing else proves the69// store is intact; the hash is the point of the store.70fn check_objects(store &storage.Store) ![]string {71	mut problems := []string{}72	mut stmt := store.db.prepare('SELECT hash FROM objects ORDER BY hash')!73	defer {74		stmt.finalize()75	}76	mut hashes := []string{}77	for stmt.step()! {78		hashes << stmt.text(0)79	}80	for hash in hashes {81		store.get_object(hash) or { problems << 'object ${hash}: ${err.msg()}' }82	}83	return problems84}8586// check_save_files catches a save naming an object that is gone — the one87// inconsistency the schema cannot prevent on its own, because objects are88// shared between saves.89fn check_save_files(store &storage.Store) ![]string {90	mut problems := []string{}91	mut stmt := store.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f92		LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')!93	defer {94		stmt.finalize()95	}96	for stmt.step()! {97		problems << 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}'98	}99	return problems100}101102// sweep_peek_folders clears the scratch trees `koh peek` leaves in the system103// temp directory.104fn sweep_peek_folders() int {105	mut removed := 0106	for name in os.ls(os.temp_dir()) or { return 0 } {107		if !name.starts_with(peek_prefix) {108			continue109		}110		os.rmdir_all(os.join_path(os.temp_dir(), name)) or { continue }111		removed++112	}113	return removed114}115116struct DoctorJson {117mut:118	problems             []string119	objects              int120	saves                int121	checkpoints          int122	cached_files         int123	peek_folders_removed int124	backup               string125}