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 111 112 if unapplied_strip(store, mut backend, name)! {113 return error('the remote has a strip this repo has not applied — run koh steal')114 }115 if remote_head.found {116 theirs := remote_head.body.bytestr().trim_space()117 newer := saves_after(store, local.id, theirs) or {118 return error('remote has new saves — run koh steal')119 }120 for save in newer {121 upload_save(store, mut backend, name, save, mut report)!122 }123 } else {124 oldest_first := history.line(store)!125 for index := oldest_first.len - 1; index >= 0; index-- {126 upload_save(store, mut backend, name, oldest_first[index], mut report)!127 }128 }129 upload_events(store, mut backend, name, mut report)!130 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {131 return error('remote has new saves — run koh steal')132 }133 store.set_state(remote_head_key, local.id)!134 return report135}136137138139140fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {141 mut newer := []history.Save{}142 mut cursor := ours143 for cursor != '' && cursor != theirs {144 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }145 newer << save146 cursor = save.parent147 }148 if cursor != theirs {149 return error('${theirs} is not on this line')150 }151 newer.reverse_in_place()152 return newer153}154155fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport) ! {156 files := history.files_of(store, save.id)!157 158 159 160 mut sent := map[string]bool{}161 if save.parent != '' {162 for file in history.files_of(store, save.parent)! {163 sent[file.hash] = true164 }165 }166 for file in files {167 if file.hash in sent {168 continue169 }170 sent[file.hash] = true171 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {172 report.objects++173 }174 }175 record := save_record(store, save)!176 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {177 report.saves++178 }179}180181182183fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {184 already := backend.list(events_prefix(repo))!185 mut already_sent := map[string]bool{}186 for key in already {187 already_sent[os.base(key)] = true188 }189 for event in events.all(store)! {190 if event.id in already_sent {191 continue192 }193 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {194 report.events++195 }196 }197}198199200201202pub struct Fetched {203pub mut:204 repo_objects map[string][]u8205 saves []SaveRecord 206 events_out []events.Event207 head string208 base string 209}210211pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {212 check_format(mut backend, repo)!213 remote_head := backend.get(head_key(repo))!214 if !remote_head.found {215 return error('the remote has no "${repo}" yet — run koh offer here first')216 }217 mut fetched := Fetched{218 head: remote_head.body.bytestr().trim_space()219 }220 mut cursor := fetched.head221 for cursor != '' && history.by_id(store, cursor) == none {222 record_object := backend.get(save_key(repo, cursor))!223 if !record_object.found {224 return error('the remote is missing save ${cursor} — run koh offer there')225 }226 record := decode_save(record_object.body)!227 for file in record.files {228 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {229 blob := backend.get(object_key(repo, file.hash))!230 if !blob.found {231 return error('the remote is missing object ${file.hash} that save ${record.id} needs')232 }233 fetched.repo_objects[file.hash] = blob.body234 }235 }236 fetched.saves << record237 cursor = record.parent238 }239 fetched.saves.reverse_in_place()240 fetched.base = cursor241 fetched.events_out = fetch_events(store, mut backend, repo)!242 return fetched243}244245fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {246 have := events.all(store)!247 mut known := map[string]bool{}248 for event in have {249 known[event.id] = true250 }251 mut incoming := []events.Event{}252 for key in backend.list(events_prefix(repo))! {253 id := os.base(key)254 if id in known {255 continue256 }257 body := backend.get(event_key(repo, id))!258 if !body.found {259 continue260 }261 incoming << decode_event(body.body)!262 }263 return incoming264}265266267268269270pub fn write_fetched(store &storage.Store, fetched Fetched) ! {271 for hash, blob in fetched.repo_objects {272 store.put_object_blob(hash, blob)!273 }274 mut strips := []events.Event{}275 for event in fetched.events_out {276 if !events.receive(store, event)! {277 continue278 }279 if event.kind == events.kind_strip {280 281 282 remove_path(store, event.path, event.save_id)!283 strips << event284 }285 }286 for record in fetched.saves {287 files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {288 return !strips.any(reaches(it, record.id, record.created_at)289 && covers(it.path, file.path))290 })291 history.import_save(store, to_save(record), files)!292 }293 if strips.len > 0 {294 store.drop_unreferenced_objects()!295 }296}297298299300fn reaches(strip events.Event, save_id string, created_at i64) bool {301 if strip.save_id != '' {302 return strip.save_id == save_id303 }304 return created_at <= strip.created_at305}