1234567891011module exchange1213import os14import src.events15import src.history16import src.remote17import src.snapshot18import src.storage192021const repo_state_key = 'remote_repo'22232425const remote_head_key = 'remote_head'2627pub const no_saves = 'this repo has no saves yet — nothing to offer'28293031const format_value = 'koh-remote 1'3233343536fn put_format(mut backend remote.Backend, repo string) ! {37 if backend.put_if_absent(format_key(repo), format_value.bytes())! {38 return39 }40 check_format(mut backend, repo)!41}4243444546fn check_format(mut backend remote.Backend, repo string) !string {47 existing := backend.get(format_key(repo))!48 if !existing.found {49 return ''50 }51 value := existing.body.bytestr().trim_space()52 if value != format_value {53 return error('the remote "${repo}" is "${value}" — this koh speaks "${format_value}"; update koh')54 }55 return value56}5758pub struct OfferReport {59pub mut:60 repo string61 saves int62 objects int63 events int64 head string65}66676869pub fn remember_repo(store &storage.Store, name string) ! {70 store.set_state(repo_state_key, name)!71}72737475pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {76 if remembered := store.state(repo_state_key) {77 return remembered78 }79 name := os.base(store.root)80 81 82 check_format(mut backend, name)!83 existing := backend.get(head_key(name))!84 if existing.found {85 theirs := existing.body.bytestr().trim_space()86 if history.by_id(store, theirs) == none {87 return error('the remote already has "${name}" with a history this repo does not share — rename the folder, or use another remote')88 }89 }90 store.set_state(repo_state_key, name)!91 return name92}93949596pub fn offer(store &storage.Store, mut backend remote.Backend) !OfferReport {97 name := repo_name(store, mut backend)!98 local := history.head(store) or { return error(no_saves) }99 mut report := OfferReport{100 repo: name101 head: local.id102 }103 if check_format(mut backend, name)! == '' {104 105 put_format(mut backend, name)!106 }107 remote_head := backend.get(head_key(name))!108 109 110 if unapplied_strips(store, mut backend, name)!.len > 0 {111 return error(strip_not_applied)112 }113 mut uploaded := Uploaded{}114 if remote_head.found {115 theirs := remote_head.body.bytestr().trim_space()116 newer := saves_after(store, local.id, theirs) or {117 return error('remote has new saves — run koh steal')118 }119 for save in newer {120 upload_save(store, mut backend, name, save, mut report, mut uploaded)!121 }122 } else {123 oldest_first := history.line(store)!124 for index := oldest_first.len - 1; index >= 0; index-- {125 upload_save(store, mut backend, name, oldest_first[index], mut report, mut uploaded)!126 }127 }128 upload_events(store, mut backend, name, mut report)!129 130 131 132 late := unapplied_strips(store, mut backend, name)!133 if late.len > 0 {134 take_back(store, mut backend, name, uploaded, late)!135 return error(strip_not_applied)136 }137 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {138 return error('remote has new saves — run koh steal')139 }140 store.set_state(remote_head_key, local.id)!141 return report142}143144145146147fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {148 mut newer := []history.Save{}149 mut cursor := ours150 for cursor != '' && cursor != theirs {151 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }152 newer << save153 cursor = save.parent154 }155 if cursor != theirs {156 return error('${theirs} is not on this line')157 }158 newer.reverse_in_place()159 return newer160}161162const strip_not_applied = 'the remote has a strip this repo has not applied — run koh steal'163164165struct Uploaded {166mut:167 saves []string168 objects map[string]bool169}170171172173fn take_back(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded, strips []events.Event) ! {174 for id in uploaded.saves {175 for file in history.files_of(store, id)! {176 if file.hash in uploaded.objects && strips.any(reaches(it, id) && covers(it.path, file.path)) {177 backend.delete(object_key(repo, file.hash))!178 }179 }180 backend.delete(save_key(repo, id))!181 }182}183184185186187fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport, mut uploaded Uploaded) ! {188 files := history.files_of(store, save.id)!189 190 191 192 mut sent := map[string]bool{}193 if save.parent != '' {194 for file in history.files_of(store, save.parent)! {195 sent[file.hash] = true196 }197 }198 for file in files {199 if file.hash in sent {200 continue201 }202 sent[file.hash] = true203 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {204 report.objects++205 uploaded.objects[file.hash] = true206 }207 }208 record := save_record(store, save)!209 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {210 report.saves++211 uploaded.saves << save.id212 }213}214215216217fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {218 already := backend.list(events_prefix(repo))!219 mut already_sent := map[string]bool{}220 for key in already {221 already_sent[os.base(key)] = true222 }223 for event in events.all(store)! {224 if event.id in already_sent {225 continue226 }227 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {228 report.events++229 }230 }231}232233234235236pub struct Fetched {237pub mut:238 repo_objects map[string][]u8239 saves []SaveRecord 240 events_out []events.Event241 head string242 base string 243 unsafe_paths []string244}245246247248249pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {250 check_format(mut backend, repo)!251 remote_head := backend.get(head_key(repo))!252 if !remote_head.found {253 return error('the remote has no "${repo}" yet — run koh offer here first')254 }255 mut fetched := Fetched{256 head: remote_head.body.bytestr().trim_space()257 }258 mut on_line := map[string]bool{}259 for save in history.line(store)! {260 on_line[save.id] = true261 }262 mut cursor := fetched.head263 for cursor != '' && cursor !in on_line {264 mut record := if saved := history.by_id(store, cursor) {265 save_record(store, saved)!266 } else {267 record_object := backend.get(save_key(repo, cursor))!268 if !record_object.found {269 return error('the remote is missing save ${cursor} — run koh offer there')270 }271 decode_save(record_object.body)!272 }273 fetched.unsafe_paths << drop_unsafe_paths(mut record)274 for file in record.files {275 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {276 blob := backend.get(object_key(repo, file.hash))!277 if !blob.found {278 return error('the remote is missing object ${file.hash} that save ${record.id} needs')279 }280 fetched.repo_objects[file.hash] = blob.body281 }282 }283 fetched.saves << record284 cursor = record.parent285 }286 fetched.saves.reverse_in_place()287 fetched.base = cursor288 fetched.events_out = fetch_events(store, mut backend, repo)!289 return fetched290}291292fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {293 have := events.all(store)!294 mut known := map[string]bool{}295 for event in have {296 known[event.id] = true297 }298 mut incoming := []events.Event{}299 for key in backend.list(events_prefix(repo))! {300 id := os.base(key)301 if id in known {302 continue303 }304 body := backend.get(event_key(repo, id))!305 if !body.found {306 continue307 }308 event := decode_event(body.body)!309 if !ignored_strip(event) {310 incoming << event311 }312 }313 return incoming314}315316317318319pub fn write_fetched(store &storage.Store, fetched Fetched) ! {320 for hash, blob in fetched.repo_objects {321 store.put_object_blob(hash, blob)!322 }323 mut stripped := false324 for event in fetched.events_out {325 if !events.receive(store, event)! {326 continue327 }328 if event.kind == events.kind_strip {329 remove_path(store, event.path, event.save_id)!330 stripped = true331 }332 }333 strips := known_strips(store, fetched)!334 for record in fetched.saves {335 import_arriving(store, record, strips)!336 }337 if stripped {338 store.drop_unreferenced_objects()!339 }340}341342343344pub fn write_fetched_records(store &storage.Store, fetched Fetched) ! {345 for hash, blob in fetched.repo_objects {346 store.put_object_blob(hash, blob)!347 }348 strips := known_strips(store, fetched)!349 for record in fetched.saves {350 import_arriving(store, record, strips)!351 }352}353354355356fn known_strips(store &storage.Store, fetched Fetched) ![]events.Event {357 mut strips := events.strip_events(store)!358 strips << fetched.events_out.filter(it.kind == events.kind_strip)359 return strips360}361362363364fn import_arriving(store &storage.Store, record SaveRecord, strips []events.Event) ! {365 if history.by_id(store, record.id) != none {366 return367 }368 files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {369 return snapshot.legal_path(file.path)370 && !strips.any(reaches(it, record.id) && covers(it.path, file.path))371 })372 history.import_save(store, to_save(record), files)!373}374375376377fn reaches(strip events.Event, save_id string) bool {378 return strip.save_id == '' || strip.save_id == save_id379}