1234module cli56import compress.zlib7import os8import src.exchange9import src.fmt10import src.history11import src.machine12import src.snapshot13import src.storage1415struct DoctorRepair {16 action string17 run fn () ! @[required]18}1920fn plan_repairs(store &storage.Store, report DoctorJson) []DoctorRepair {21 mut repairs := []DoctorRepair{}22 has_remote := report.source != '' || machine.has_remote()23 24 writable_remote := report.source == '' && machine.has_remote()25 source, remote_repo := report.source, report.remote_repo26 if report.checks.any(it.name == 'SQLite integrity' && it.status == 'fail') {27 repairs << DoctorRepair{'rebuild the database indexes', fn [store] () ! {28 store.db.exec('REINDEX')!29 }}30 }31 if report.damaged_objects.len > 0 {32 hashes := report.damaged_objects33 repairs << DoctorRepair{'restore ${fmt.plural(hashes.len, 'damaged or missing object')} from verified copies', fn [store, hashes, source, remote_repo] () ! {34 repair_objects(store, hashes, source, remote_repo)!35 }}36 }37 if report.missing_saves.len > 0 && has_remote {38 ids := report.missing_saves39 repairs << DoctorRepair{'fetch ${fmt.plural(ids.len, 'missing save')} from the remote', fn [store, ids, source, remote_repo] () ! {40 mut backend, repo := doctor_backend(store, source, remote_repo)!41 exchange.fetch_missing(store, mut backend, repo, ids)!42 }}43 } else if report.metadata_available && report.saves > 0 && report.head.id == '' {44 45 46 audited := !report.checks.any(it.name in ['remote audit', 'remote stability']47 && it.status != 'pass')48 tip := only_tip(store) or { '' }49 seen := report.head_id50 if tip != '' && (!has_remote || (audited && report.remote_head in ['', tip])) {51 repairs << DoctorRepair{'point head at [${tip}], the only save nothing follows', fn [store, tip, seen, has_remote, source, remote_repo] () ! {52 if has_remote {53 mut backend, repo := doctor_backend(store, source, remote_repo)!54 theirs := backend.get(exchange.head_key(repo))!55 if theirs.found && theirs.body.bytestr().trim_space() != tip {56 return error('the remote now has another head; run koh doctor again')57 }58 }59 store.begin()!60 set_only_tip(store, tip, seen) or {61 store.rollback()62 return err63 }64 store.end()!65 }}66 }67 }68 if report.strip_residue {69 repairs << DoctorRepair{'remove stripped paths that saves still hold', fn [store] () ! {70 exchange.reapply_strips(store)!71 }}72 }73 if report.restore_unfinished {74 repairs << DoctorRepair{'finish the interrupted restore (koh revert keeps the current files in a checkpoint)', fn [store] () ! {75 finish_restore(store)!76 }}77 }78 if report.pending_strips.len > 0 && writable_remote {79 paths := report.pending_strips80 repairs << DoctorRepair{'send ${fmt.plural(paths.len, 'unfinished strip')} to the remote', fn [store, paths, remote_repo] () ! {81 send_strips(store, remote_repo, paths, false)!82 }}83 }84 85 86 if report.remote_strip_residue.len > 0 && writable_remote {87 paths := report.remote_strip_residue88 repairs << DoctorRepair{'remove ${fmt.plural(paths.len, 'stripped path')} the remote still holds', fn [store, paths, remote_repo] () ! {89 send_strips(store, remote_repo, paths, true)!90 }}91 }92 93 if report.unreferenced_objects > 0 && report.problems.len == 0 {94 repairs << DoctorRepair{'delete ${fmt.plural(int(report.unreferenced_objects), 'object')} no save uses', fn [store] () ! {95 store.begin()!96 store.drop_unreferenced_objects() or {97 store.rollback()98 return err99 }100 store.end()!101 }}102 }103 if writable_remote {104 hashes := report.remote_bad_objects.filter(stored_intact(store, it))105 ids := report.remote_missing_saves.filter(history.by_id(store, it) != none)106 if hashes.len + ids.len > 0 {107 repairs << DoctorRepair{'upload ${fmt.plural(hashes.len, 'object')} and ${fmt.plural(ids.len, 'save')} the remote lacks or holds damaged', fn [store, hashes, ids, remote_repo] () ! {108 mut backend, repo := doctor_backend(store, '', remote_repo)!109 exchange.resend(store, mut backend, repo, hashes, ids)!110 }}111 }112 }113 return repairs114}115116117118fn repair_objects(store &storage.Store, hashes []string, source string, remote_repo string) ! {119 mut lost := []string{}120 for index, hash in hashes {121 restored := restore_object(store, hash, source, remote_repo) or {122 return exchange.partway(done_text('restored', index - lost.len, 'damaged or missing object'),123 err)124 }125 if !restored {126 lost << hash127 }128 }129 if lost.len > 0 {130 failure := error('no verified copy here or on the remote of ${lost.join(', ')}')131 return exchange.partway(done_text('restored', hashes.len - lost.len, 'damaged or missing object'),132 failure)133 }134}135136fn send_strips(store &storage.Store, remote_repo string, paths []string, requeue bool) ! {137 mut backend, _ := doctor_backend(store, '', remote_repo)!138 for index, path in paths {139 if requeue {140 exchange.requeue_strip(store, path) or {141 return exchange.partway(done_text('sent', index, 'strip'), err)142 }143 }144 exchange.send_strip(store, mut backend, path) or {145 return exchange.partway(done_text('sent', index, 'strip'), err)146 }147 }148}149150151fn done_text(verb string, done int, noun string) []string {152 return if done == 0 { []string{} } else { ['${verb} ${fmt.plural(done, noun)}'] }153}154155fn restore_object(store &storage.Store, hash string, source string, remote_repo string) !bool {156 if data := local_copy(store, hash) {157 store.repair_object(hash, data)!158 return true159 }160 if source == '' && !machine.has_remote() {161 return false162 }163 mut backend, repo := doctor_backend(store, source, remote_repo)!164 blob := backend.get(exchange.object_key(repo, hash))!165 data := zlib.decompress(blob.body) or { return false }166 if !blob.found || storage.hash_hex(data) != hash {167 return false168 }169 store.repair_object(hash, data)!170 return true171}172173fn stored_intact(store &storage.Store, hash string) bool {174 store.get_object(hash) or { return false }175 return true176}177178fn local_copy(store &storage.Store, hash string) ?[]u8 {179 if data := store.get_object(hash) {180 return data181 }182 for path in store.paths_of_object(hash) or { return none } {183 if !snapshot.legal_path(path) {184 continue185 }186 data := os.read_bytes(os.join_path(store.root, path)) or { continue }187 if storage.hash_hex(data) == hash {188 return data189 }190 }191 return none192}193194195196fn only_tip(store &storage.Store) ?string {197 saves := history.all_saves(store) or { return none }198 mut parents := map[string]bool{}199 for save in saves {200 parents[save.parent] = true201 }202 tips := saves.filter(it.id !in parents)203 return if tips.len == 1 { tips[0].id } else { none }204}205206207208fn set_only_tip(store &storage.Store, tip string, seen string) ! {209 if (store.state(storage.head_key) or { '' }) != seen || (only_tip(store) or { '' }) != tip {210 return error('history changed since Doctor checked it; run koh doctor again')211 }212 history.set_head(store, tip)!213}214215216fn finish_restore(store &storage.Store) ! {217 head := history.head(store) or { return error('this repo has no head to restore') }218 if !compare_with_head(store, head)!.dirty {219 snapshot.clear_restore_mark(store)!220 return221 }222 plan := plan_restore(store, head)!223 apply_restore(store, head, 'revert', plan, true)!224 snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!225}