12345678module cli910import json211import os12import time13import src.fmt14import src.history15import src.snapshot16import src.storage1718pub fn cmd_doctor(raw []string) int {19 args := parse('doctor', raw, Options{20 values: ['backup']21 flags: ['json']22 }) or { return usage_error(err.msg()) }23 store := open_store() or { return refuse(args.json(), err.msg()) }24 defer {25 store.close()26 }27 mut report := DoctorJson{28 problems: []string{}29 }3031 report.problems << store.integrity_check() or { return refuse(args.json(), err.msg()) }32 report.problems << store.check_objects() or { return refuse(args.json(), err.msg()) }33 report.problems << store.check_save_files() or { return refuse(args.json(), err.msg()) }34 report.problems << unsafe_save_paths(store) or { return refuse(args.json(), err.msg()) }3536 report.objects = int(store.object_count() or { 0 })37 report.saves = int(history.save_count(store) or { 0 })38 report.checkpoints = int(history.checkpoint_count(store) or { 0 })39 report.cached_files = int(store.cache_count() or { 0 })40 report.peek_folders_removed = sweep_peek_folders()41 store.drop_cache() or { return refuse(args.json(), err.msg()) }4243 if path := args.value_or_none('backup') {44 if os.exists(path) {45 return refuse(args.json(), '${path} already exists')46 }47 store.backup(path) or { return refuse(args.json(), err.msg()) }48 report.backup = path49 }5051 if args.json() {52 emit_json(json2.encode(report))53 } else {54 if report.problems.len == 0 {55 ok('${store.root} is sound')56 } else {57 for problem in report.problems {58 warn(problem)59 }60 }61 note('${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.checkpoints, 'checkpoint')}')62 if report.backup != '' {63 ok('backed up to ${report.backup}')64 }65 }66 if report.problems.len > 0 {67 return 168 }69 return 070}7172fn unsafe_save_paths(store &storage.Store) ![]string {73 mut problems := []string{}74 for save in history.line(store)! {75 for file in history.files_of(store, save.id)! {76 if !snapshot.legal_path(file.path) {77 problems << 'save [${save.id}] has unsafe path ${snapshot.escape_path(file.path)} — koh strip ${shell_quote(file.path)}'78 }79 }80 }81 return problems82}8384fn shell_quote(text string) string {85 return "'${text.replace("'", "'\\''")}'"86}878889const peek_age = 8640090919293fn sweep_peek_folders() int {94 mut removed := 095 for name in os.ls(os.temp_dir()) or { return 0 } {96 path := os.join_path(os.temp_dir(), name)97 if !name.starts_with(peek_prefix)98 || !peek_is_old(os.file_last_mod_unix(path), time.now().unix()) {99 continue100 }101 os.rmdir_all(path) or { continue }102 removed++103 }104 return removed105}106107108109fn peek_is_old(modified i64, now i64) bool {110 return modified < now - peek_age111}112113struct DoctorJson {114mut:115 problems []string116 objects int117 saves int118 checkpoints int119 cached_files int120 peek_folders_removed int121 backup string122}