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 if report.upload_unsettled && writable_remote {93 repairs << DoctorRepair{'delete what an interrupted upload sent that a strip removed', fn [store, remote_repo] () ! {94 mut backend, repo := doctor_backend(store, '', remote_repo)!95 exchange.settle_unsettled(store, mut backend, repo)!96 }}97 }98 99 if report.unreferenced_objects > 0 && report.problems.len == 0 {100 repairs << DoctorRepair{'delete ${fmt.plural(int(report.unreferenced_objects), 'object')} no save uses', fn [store] () ! {101 store.begin()!102 store.drop_unreferenced_objects() or {103 store.rollback()104 return err105 }106 store.end()!107 }}108 }109 if writable_remote {110 hashes := report.remote_bad_objects.filter(stored_intact(store, it))111 ids := report.remote_missing_saves.filter(history.by_id(store, it) != none)112 if hashes.len + ids.len > 0 {113 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] () ! {114 mut backend, repo := doctor_backend(store, '', remote_repo)!115 exchange.resend(store, mut backend, repo, hashes, ids)!116 }}117 }118 }119 return repairs120}121122123124fn repair_objects(store &storage.Store, hashes []string, source string, remote_repo string) ! {125 mut lost := []string{}126 for index, hash in hashes {127 restored := restore_object(store, hash, source, remote_repo) or {128 return exchange.partway(done_text('restored', index - lost.len, 'damaged or missing object'),129 err)130 }131 if !restored {132 lost << hash133 }134 }135 if lost.len > 0 {136 failure := error('no verified copy here or on the remote of ${lost.join(', ')}')137 return exchange.partway(done_text('restored', hashes.len - lost.len, 'damaged or missing object'),138 failure)139 }140}141142fn send_strips(store &storage.Store, remote_repo string, paths []string, requeue bool) ! {143 mut backend, _ := doctor_backend(store, '', remote_repo)!144 for index, path in paths {145 if requeue {146 exchange.requeue_strip(store, path) or {147 return exchange.partway(done_text('sent', index, 'strip'), err)148 }149 }150 exchange.send_strip(store, mut backend, path) or {151 return exchange.partway(done_text('sent', index, 'strip'), err)152 }153 }154}155156157fn done_text(verb string, done int, noun string) []string {158 return if done == 0 { []string{} } else { ['${verb} ${fmt.plural(done, noun)}'] }159}160161fn restore_object(store &storage.Store, hash string, source string, remote_repo string) !bool {162 if data := local_copy(store, hash) {163 store.repair_object(hash, data)!164 return true165 }166 if source == '' && !machine.has_remote() {167 return false168 }169 mut backend, repo := doctor_backend(store, source, remote_repo)!170 blob := backend.get(exchange.object_key(repo, hash))!171 data := zlib.decompress(blob.body) or { return false }172 if !blob.found || storage.hash_hex(data) != hash {173 return false174 }175 store.repair_object(hash, data)!176 return true177}178179fn stored_intact(store &storage.Store, hash string) bool {180 store.get_object(hash) or { return false }181 return true182}183184fn local_copy(store &storage.Store, hash string) ?[]u8 {185 if data := store.get_object(hash) {186 return data187 }188 for path in store.paths_of_object(hash) or { return none } {189 if !snapshot.legal_path(path) {190 continue191 }192 data := os.read_bytes(os.join_path(store.root, path)) or { continue }193 if storage.hash_hex(data) == hash {194 return data195 }196 }197 return none198}199200201202fn only_tip(store &storage.Store) ?string {203 saves := history.all_saves(store) or { return none }204 mut parents := map[string]bool{}205 for save in saves {206 parents[save.parent] = true207 }208 tips := saves.filter(it.id !in parents)209 return if tips.len == 1 { tips[0].id } else { none }210}211212213214fn set_only_tip(store &storage.Store, tip string, seen string) ! {215 if (store.state(storage.head_key) or { '' }) != seen || (only_tip(store) or { '' }) != tip {216 return error('history changed since Doctor checked it; run koh doctor again')217 }218 history.set_head(store, tip)!219}220221222fn finish_restore(store &storage.Store) ! {223 head := history.head(store) or { return error('this repo has no head to restore') }224 if !compare_with_head(store, head)!.dirty {225 snapshot.clear_restore_mark(store)!226 return227 }228 plan := plan_restore(store, head)!229 apply_restore(store, head, 'revert', plan, true)!230 snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!231}