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', 'yes']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 working('checking ${fmt.plural(int(store.object_count() or { 0 }), 'object')} and every save')32 report.problems << store.integrity_check() or { return refuse(args.json(), err.msg()) }33 report.problems << store.check_objects() or { return refuse(args.json(), err.msg()) }34 report.problems << store.check_save_files() or { return refuse(args.json(), err.msg()) }35 report.problems << unsafe_save_paths(store) or { return refuse(args.json(), err.msg()) }3637 report.objects = int(store.object_count() or { 0 })38 report.saves = int(history.save_count(store) or { 0 })39 report.checkpoints = int(history.checkpoint_count(store) or { 0 })40 report.cached_files = int(store.cache_count() or { 0 })41 if args.flag('yes') {42 report.peek_folders_removed = sweep_peek_folders()43 }44 store.drop_cache() or { return refuse(args.json(), err.msg()) }4546 if path := args.value_or_none('backup') {47 if os.exists(path) {48 return refuse(args.json(), '${path} already exists')49 }50 store.backup(path) or { return refuse(args.json(), err.msg()) }51 report.backup = path52 }5354 if args.json() {55 emit_json(json2.encode(report))56 } else {57 if report.problems.len == 0 {58 ok('${store.root} is sound')59 } else {60 for problem in report.problems {61 warn(problem)62 }63 }64 note('${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.checkpoints, 'checkpoint')}')65 if report.backup != '' {66 ok('backed up to ${report.backup}')67 }68 }69 if report.problems.len > 0 {70 return 171 }72 return 073}7475fn unsafe_save_paths(store &storage.Store) ![]string {76 mut problems := []string{}77 for save in history.line(store)! {78 for file in history.files_of(store, save.id)! {79 if !snapshot.legal_path(file.path) {80 problems << 'save [${save.id}] has unsafe path ${snapshot.escape_path(file.path)} — koh strip ${shell_quote(file.path)}'81 }82 }83 }84 return problems85}8687fn shell_quote(text string) string {88 return "'${text.replace("'", "'\\''")}'"89}909192const peek_age = 8640093949596fn sweep_peek_folders() int {97 mut removed := 098 for name in os.ls(os.temp_dir()) or { return 0 } {99 path := os.join_path(os.temp_dir(), name)100 info := os.lstat(path) or { continue }101 if !name.starts_with(peek_prefix) || os.is_link(path) || !os.is_dir(path)102 || info.uid != u32(os.getuid()) || !peek_is_old(os.file_last_mod_unix(path), time.now().unix()) {103 continue104 }105 os.rmdir_all(path) or { continue }106 removed++107 }108 return removed109}110111112113fn peek_is_old(modified i64, now i64) bool {114 return modified < now - peek_age115}116117struct DoctorJson {118mut:119 problems []string120 objects int121 saves int122 checkpoints int123 cached_files int124 peek_folders_removed int125 backup string126}