cmd_doctor.v

3.8 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 time13import src.history14import src.storage1516pub fn cmd_doctor(raw []string) int {17	args := parse('doctor', raw, Options{18		values: ['backup']19		flags:  ['json']20	}) or { return usage_error(err.msg()) }21	store := open_store() or { return refuse(args.json(), err.msg()) }22	defer {23		store.close()24	}25	mut report := DoctorJson{26		problems: []string{}27	}2829	report.problems << store.integrity_check() or { return refuse(args.json(), err.msg()) }30	report.problems << check_objects(store) or { return refuse(args.json(), err.msg()) }31	report.problems << check_save_files(store) or { return refuse(args.json(), err.msg()) }3233	report.objects = int(store.object_count() or { 0 })34	report.saves = int(history.save_count(store) or { 0 })35	report.checkpoints = int(history.checkpoint_count(store) or { 0 })36	report.cached_files = int(store.cache_count() or { 0 })37	report.peek_folders_removed = sweep_peek_folders()38	store.drop_cache() or { return refuse(args.json(), err.msg()) }3940	if path := args.value_or_none('backup') {41		if os.exists(path) {42			return refuse(args.json(), '${path} already exists')43		}44		store.backup(path) or { return refuse(args.json(), err.msg()) }45		report.backup = path46	}4748	if args.json() {49		emit_json(json.encode(report))50	} else {51		if report.problems.len == 0 {52			ok('${store.root} is sound')53		} else {54			for problem in report.problems {55				warn(problem)56			}57		}58		note('${report.objects} objects · ${report.saves} saves · ${report.checkpoints} checkpoints')59		if report.backup != '' {60			ok('backed up to ${report.backup}')61		}62	}63	if report.problems.len > 0 {64		return 165	}66	return 067}6869// check_objects reads every object and hashes it back. Nothing else proves the70// store is intact; the hash is the point of the store.71fn check_objects(store &storage.Store) ![]string {72	mut problems := []string{}73	mut stmt := store.db.prepare('SELECT hash FROM objects ORDER BY hash')!74	defer {75		stmt.finalize()76	}77	mut hashes := []string{}78	for stmt.step()! {79		hashes << stmt.text(0)80	}81	for hash in hashes {82		store.get_object(hash) or { problems << 'object ${hash}: ${err.msg()}' }83	}84	return problems85}8687// check_save_files catches a save naming an object that is gone — the one88// inconsistency the schema cannot prevent on its own, because objects are89// shared between saves.90fn check_save_files(store &storage.Store) ![]string {91	mut problems := []string{}92	mut stmt := store.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f93		LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')!94	defer {95		stmt.finalize()96	}97	for stmt.step()! {98		problems << 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}'99	}100	return problems101}102103// peek_age is how old a peek folder must be before doctor clears it.104const peek_age = 86400105106// sweep_peek_folders clears the scratch trees `koh peek` leaves in the system107// temp directory, once they are a day old.108fn sweep_peek_folders() int {109	mut removed := 0110	for name in os.ls(os.temp_dir()) or { return 0 } {111		path := os.join_path(os.temp_dir(), name)112		// A peek from today may still be open in somebody's editor.113		if !name.starts_with(peek_prefix)114			|| os.file_last_mod_unix(path) > time.now().unix() - peek_age {115			continue116		}117		os.rmdir_all(path) or { continue }118		removed++119	}120	return removed121}122123struct DoctorJson {124mut:125	problems             []string126	objects              int127	saves                int128	checkpoints          int129	cached_files         int130	peek_folders_removed int131	backup               string132}