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}7273747576pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {77 if remembered := store.state(repo_state_key) {78 return follow_rename(store, mut backend, remembered)!79 }80 name := os.base(store.root).to_lower()81 82 83 check_format(mut backend, name)!84 existing := backend.get(head_key(name))!85 if existing.found {86 theirs := existing.body.bytestr().trim_space()87 if history.by_id(store, theirs) == none {88 return error('the remote already has "${name}" with a history this repo does not share — rename the folder, or use another remote')89 }90 }91 store.set_state(repo_state_key, name)!92 return name93}9495969798fn follow_rename(store &storage.Store, mut backend remote.Backend, remembered string) !string {99 mut name := remembered100 for _ in 0 .. 5 {101 if backend.get(head_key(name))!.found {102 break103 }104 marker := backend.get('.kepr/renamed/${name}')!105 next := marker.body.bytestr().trim_space()106 if !marker.found || next == '' || next.contains('/') || next.contains('\\') || next in [107 '.',108 '..',109 ] {110 break111 }112 name = next113 }114 if name != remembered {115 store.set_state(repo_state_key, name)!116 }117 return name118}119120121122pub fn offer(store &storage.Store, mut backend remote.Backend) !OfferReport {123 name := repo_name(store, mut backend)!124 local := history.head(store) or { return error(no_saves) }125 mut report := OfferReport{126 repo: name127 head: local.id128 }129 if check_format(mut backend, name)! == '' {130 131 put_format(mut backend, name)!132 }133 remote_head := backend.get(head_key(name))!134 135 136 if unapplied_strips(store, mut backend, name)!.len > 0 {137 return error(strip_not_applied)138 }139 mut uploaded := Uploaded{}140 upload_line(store, mut backend, name, local.id, remote_head, mut report, mut uploaded) or {141 return after_failed_upload(store, mut backend, name, uploaded, false, err)142 }143 144 145 146 if undo_late_strips(store, mut backend, name, uploaded, false)! {147 return error(strip_not_applied)148 }149 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {150 return error('remote has new saves — run koh steal')151 }152 store.set_state(remote_head_key, local.id)!153 return report154}155156157158159fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {160 mut newer := []history.Save{}161 mut cursor := ours162 for cursor != '' && cursor != theirs {163 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }164 newer << save165 cursor = save.parent166 }167 if cursor != theirs {168 return error('${theirs} is not on this line')169 }170 newer.reverse_in_place()171 return newer172}173174const strip_not_applied = 'the remote has a strip this repo has not applied — run koh steal'175176177struct Uploaded {178mut:179 saves []string180 objects map[string]bool181}182183184fn upload_line(store &storage.Store, mut backend remote.Backend, name string, local string, remote_head remote.Object, mut report OfferReport, mut uploaded Uploaded) ! {185 if remote_head.found {186 theirs := remote_head.body.bytestr().trim_space()187 newer := saves_after(store, local, theirs) or {188 return error('remote has new saves — run koh steal')189 }190 for save in newer {191 upload_save(store, mut backend, name, save, mut report, mut uploaded)!192 }193 } else {194 oldest_first := history.line(store)!195 for index := oldest_first.len - 1; index >= 0; index-- {196 upload_save(store, mut backend, name, oldest_first[index], mut report, mut uploaded)!197 }198 }199 upload_events(store, mut backend, name, mut report)!200}201202203204fn after_failed_upload(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded, keep_records bool, failure IError) IError {205 undo_late_strips(store, mut backend, repo, uploaded, keep_records) or {206 return error('${failure.msg()}; removing what a strip on the remote covers also failed: ${err.msg()}')207 }208 return failure209}210211212213214215216fn undo_late_strips(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded, keep_records bool) !bool {217 strips := unapplied_strips(store, mut backend, repo)!218 if strips.len == 0 {219 return false220 }221 for id in uploaded.saves {222 key := save_key(repo, id)223 if !keep_records {224 backend.delete(key)!225 continue226 }227 object := backend.get(key)!228 if !object.found {229 continue230 }231 record := decode_save(object.body)!232 kept := record.files.filter(fn [strips, id] (file FileRecord) bool {233 return !strips.any(reaches(it, id) && covers(it.path, file.path))234 })235 if kept.len < record.files.len {236 backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), object.tag)!237 }238 }239 drop_stripped_objects(store, mut backend, repo, uploaded, strips)!240 return true241}242243244245246fn drop_stripped_objects(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded, strips []events.Event) ! {247 mut reached := map[string]bool{}248 for save in history.all_saves(store)! {249 for file in history.files_of(store, save.id)! {250 if file.hash in uploaded.objects && strips.any(reaches(it, save.id) && covers(it.path, file.path)) {251 reached[file.hash] = true252 }253 }254 }255 if reached.len == 0 {256 return257 }258 for key in backend.list('${repo}/saves')! {259 object := backend.get(key)!260 if object.found {261 for file in decode_save(object.body)!.files {262 reached.delete(file.hash)263 }264 }265 }266 for hash, _ in reached {267 backend.delete(object_key(repo, hash))!268 }269}270271272273274fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport, mut uploaded Uploaded) ! {275 files := history.files_of(store, save.id)!276 277 278 279 mut sent := map[string]bool{}280 if save.parent != '' {281 for file in history.files_of(store, save.parent)! {282 sent[file.hash] = true283 }284 }285 for file in files {286 if file.hash in sent {287 continue288 }289 sent[file.hash] = true290 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {291 report.objects++292 uploaded.objects[file.hash] = true293 }294 }295 record := save_record(store, save)!296 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {297 report.saves++298 uploaded.saves << save.id299 }300}301302303304fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {305 already := backend.list(events_prefix(repo))!306 mut already_sent := map[string]bool{}307 for key in already {308 already_sent[os.base(key)] = true309 }310 for event in events.all(store)! {311 if event.id in already_sent {312 continue313 }314 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {315 report.events++316 }317 }318}319320321322323pub struct Fetched {324pub mut:325 repo_objects map[string][]u8326 saves []SaveRecord 327 events_out []events.Event328 head string329 base string 330 unsafe_paths []string331}332333334335336pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {337 check_format(mut backend, repo)!338 remote_head := backend.get(head_key(repo))!339 if !remote_head.found {340 return error('the remote has no "${repo}" yet — run koh offer here first')341 }342 mut fetched := Fetched{343 head: remote_head.body.bytestr().trim_space()344 }345 mut on_line := map[string]bool{}346 for save in history.line(store)! {347 on_line[save.id] = true348 }349 mut cursor := fetched.head350 for cursor != '' && cursor !in on_line {351 mut record := if saved := history.by_id(store, cursor) {352 save_record(store, saved)!353 } else {354 record_object := backend.get(save_key(repo, cursor))!355 if !record_object.found {356 return error('the remote is missing save ${cursor} — run koh offer there')357 }358 decode_save(record_object.body)!359 }360 fetched.unsafe_paths << drop_unsafe_paths(mut record)361 for file in record.files {362 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {363 blob := backend.get(object_key(repo, file.hash))!364 if !blob.found {365 return error('the remote is missing object ${file.hash} that save ${record.id} needs')366 }367 fetched.repo_objects[file.hash] = blob.body368 }369 }370 fetched.saves << record371 cursor = record.parent372 }373 fetched.saves.reverse_in_place()374 fetched.base = cursor375 fetched.events_out = fetch_events(store, mut backend, repo)!376 return fetched377}378379fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {380 have := events.all(store)!381 mut known := map[string]bool{}382 for event in have {383 known[event.id] = true384 }385 mut incoming := []events.Event{}386 for key in backend.list(events_prefix(repo))! {387 id := os.base(key)388 if id in known {389 continue390 }391 body := backend.get(event_key(repo, id))!392 if !body.found {393 continue394 }395 event := decode_event(body.body)!396 if !ignored_strip(event) {397 incoming << event398 }399 }400 return incoming401}402403404405406pub fn write_fetched(store &storage.Store, fetched Fetched) ! {407 for hash, blob in fetched.repo_objects {408 store.put_object_blob(hash, blob)!409 }410 mut stripped := false411 for event in fetched.events_out {412 if !events.receive(store, event)! {413 continue414 }415 if events.valid_strip(event) {416 remove_path(store, event.path, event.save_id)!417 stripped = true418 }419 }420 strips := known_strips(store, fetched)!421 for record in fetched.saves {422 import_arriving(store, record, strips)!423 }424 if stripped {425 store.drop_unreferenced_objects()!426 }427}428429430431pub fn write_fetched_records(store &storage.Store, fetched Fetched) ! {432 for hash, blob in fetched.repo_objects {433 store.put_object_blob(hash, blob)!434 }435 strips := known_strips(store, fetched)!436 for record in fetched.saves {437 import_arriving(store, record, strips)!438 }439}440441442443fn known_strips(store &storage.Store, fetched Fetched) ![]events.Event {444 mut strips := events.strip_events(store)!445 strips << fetched.events_out446 return strips.filter(events.valid_strip(it))447}448449450451fn import_arriving(store &storage.Store, record SaveRecord, strips []events.Event) ! {452 if history.by_id(store, record.id) != none {453 return454 }455 files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {456 return snapshot.legal_path(file.path)457 && !strips.any(reaches(it, record.id) && covers(it.path, file.path))458 })459 history.import_save(store, to_save(record), files)!460}461462463464fn reaches(strip events.Event, save_id string) bool {465 return strip.save_id == '' || strip.save_id == save_id466}