//! `koh doctor` — integrity check and cleanup. //! //! One source of truth means there is nothing to rebuild; what is left to check //! is that the file is sound and that every object a save names is really //! there. Doctor also tidies the things that are safe to lose: the file cache //! and old peek folders. module cli import json import os import src.history import src.storage pub fn cmd_doctor(raw []string) int { args := parse('doctor', raw, Options{ values: ['backup'] flags: ['json'] }) or { return usage_error(err.msg()) } store := open_store() or { return refuse(args.json(), err.msg()) } defer { store.close() } mut report := DoctorJson{ problems: []string{} } report.problems << store.integrity_check() or { return refuse(args.json(), err.msg()) } report.problems << check_objects(store) or { return refuse(args.json(), err.msg()) } report.problems << check_save_files(store) or { return refuse(args.json(), err.msg()) } report.objects = int(store.object_count() or { 0 }) report.saves = int(history.save_count(store) or { 0 }) report.checkpoints = int(history.checkpoint_count(store) or { 0 }) report.cached_files = int(store.cache_count() or { 0 }) report.peek_folders_removed = sweep_peek_folders() store.drop_cache() or { return refuse(args.json(), err.msg()) } if path := args.value_or_none('backup') { if os.exists(path) { return refuse(args.json(), '${path} already exists') } store.backup(path) or { return refuse(args.json(), err.msg()) } report.backup = path } if args.json() { emit_json(json.encode(report)) } else { if report.problems.len == 0 { ok('${store.root} is sound') } else { for problem in report.problems { warn(problem) } } note('${report.objects} objects · ${report.saves} saves · ${report.checkpoints} checkpoints') if report.backup != '' { ok('backed up to ${report.backup}') } } if report.problems.len > 0 { return 1 } return 0 } // check_objects reads every object and hashes it back. Nothing else proves the // store is intact; the hash is the point of the store. fn check_objects(store &storage.Store) ![]string { mut problems := []string{} mut stmt := store.db.prepare('SELECT hash FROM objects ORDER BY hash')! defer { stmt.finalize() } mut hashes := []string{} for stmt.step()! { hashes << stmt.text(0) } for hash in hashes { store.get_object(hash) or { problems << 'object ${hash}: ${err.msg()}' } } return problems } // check_save_files catches a save naming an object that is gone — the one // inconsistency the schema cannot prevent on its own, because objects are // shared between saves. fn check_save_files(store &storage.Store) ![]string { mut problems := []string{} mut stmt := store.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')! defer { stmt.finalize() } for stmt.step()! { problems << 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}' } return problems } // sweep_peek_folders clears the scratch trees `koh peek` leaves in the system // temp directory. fn sweep_peek_folders() int { mut removed := 0 for name in os.ls(os.temp_dir()) or { return 0 } { if !name.starts_with(peek_prefix) { continue } os.rmdir_all(os.join_path(os.temp_dir(), name)) or { continue } removed++ } return removed } struct DoctorJson { mut: problems []string objects int saves int checkpoints int cached_files int peek_folders_removed int backup string }