12345678module 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}68697071fn 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}8687888990fn 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}102103104const peek_age = 86400105106107108fn 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 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}