1234567891011module exchange1213import os14import src.events15import src.fmt16import src.history17import src.remote18import src.snapshot19import src.storage202122const repo_state_key = 'remote_repo'23242526const remote_head_key = 'remote_head'2728pub const no_saves = 'this repo has no saves yet — nothing to offer'29303132const format_value = 'koh-remote 1'3334353637fn put_format(mut backend remote.Backend, repo string) ! {38 if backend.put_if_absent(format_key(repo), format_value.bytes())! {39 return40 }41 check_format(mut backend, repo)!42}4344454647fn check_format(mut backend remote.Backend, repo string) !string {48 existing := backend.get(format_key(repo))!49 if !existing.found {50 return ''51 }52 value := existing.body.bytestr().trim_space()53 if value != format_value {54 return error('the remote "${repo}" is "${value}" — this koh speaks "${format_value}"; update koh')55 }56 return value57}5859pub struct OfferReport {60pub mut:61 repo string62 saves int63 objects int64 events int65 head string66}67686970pub fn remember_repo(store &storage.Store, name string) ! {71 store.set_state(repo_state_key, name)!72}7374757677pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {78 if remembered := store.state(repo_state_key) {79 return follow_rename(store, mut backend, remembered)!80 }81 name := os.base(store.root).to_lower()82 83 84 check_format(mut backend, name)!85 existing := backend.get(head_key(name))!86 if existing.found {87 theirs := existing.body.bytestr().trim_space()88 if history.by_id(store, theirs) == none {89 return error('the remote already has "${name}" with a history this repo does not share — rename the folder, or use another remote')90 }91 }92 store.set_state(repo_state_key, name)!93 return name94}9596979899fn follow_rename(store &storage.Store, mut backend remote.Backend, remembered string) !string {100 mut name := remembered101 for _ in 0 .. 5 {102 if backend.get(head_key(name))!.found {103 break104 }105 marker := backend.get('.kepr/renamed/${name}')!106 next := marker.body.bytestr().trim_space()107 if !marker.found || next == '' || next.contains('/') || next.contains('\\') || next in [108 '.',109 '..',110 ] {111 break112 }113 name = next114 }115 if name != remembered {116 store.set_state(repo_state_key, name)!117 }118 return name119}120121122123pub fn offer(store &storage.Store, mut backend remote.Backend) !OfferReport {124 name := repo_name(store, mut backend)!125 local := history.head(store) or { return error(no_saves) }126 mut report := OfferReport{127 repo: name128 head: local.id129 }130 if check_format(mut backend, name)! == '' {131 132 put_format(mut backend, name)!133 }134 remote_head := backend.get(head_key(name))!135 136 137 if unapplied_strips(store, mut backend, name)!.len > 0 {138 return error(strip_not_applied)139 }140 mut uploaded := Uploaded{}141 upload_line(store, mut backend, name, local.id, remote_head, mut report, mut uploaded) or {142 return after_failed_upload(store, mut backend, name, uploaded, err)143 }144 145 146 147 if undo_late_strips(store, mut backend, name, uploaded)! {148 return error(strip_not_applied)149 }150 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {151 return error('remote has new saves — run koh steal')152 }153 store.set_state(remote_head_key, local.id)!154 return report155}156157158159160fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {161 mut newer := []history.Save{}162 mut cursor := ours163 for cursor != '' && cursor != theirs {164 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }165 newer << save166 cursor = save.parent167 }168 if cursor != theirs {169 return error('${theirs} is not on this line')170 }171 newer.reverse_in_place()172 return newer173}174175const strip_not_applied = 'the remote has a strip this repo has not applied — run koh steal'176177178struct Uploaded {179mut:180 saves []string181 objects map[string]bool182}183184fn (u Uploaded) done() []string {185 return if u.saves.len + u.objects.len == 0 {186 []string{}187 } else {188 ['uploaded ${fmt.plural(u.objects.len, 'object')} and ${fmt.plural(u.saves.len, 'save record')}']189 }190}191192193fn upload_line(store &storage.Store, mut backend remote.Backend, name string, local string, remote_head remote.Object, mut report OfferReport, mut uploaded Uploaded) ! {194 if remote_head.found {195 theirs := remote_head.body.bytestr().trim_space()196 newer := saves_after(store, local, theirs) or {197 return error('remote has new saves — run koh steal')198 }199 for save in newer {200 upload_save(store, mut backend, name, save, mut report, mut uploaded)!201 }202 } else {203 oldest_first := history.line(store)!204 for index := oldest_first.len - 1; index >= 0; index-- {205 upload_save(store, mut backend, name, oldest_first[index], mut report, mut uploaded)!206 }207 }208 upload_events(store, mut backend, name, mut report)!209}210211212213fn after_failed_upload(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded, failure IError) IError {214 undo_late_strips(store, mut backend, repo, uploaded) or {215 return error('${failure.msg()}; removing what a strip on the remote covers also failed: ${err.msg()}')216 }217 return failure218}219220221222223224225226fn undo_late_strips(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded) !bool {227 strips := unapplied_strips(store, mut backend, repo)!228 if strips.len == 0 {229 return false230 }231 for strip in strips {232 queue_strip(store, strip.path, [strip.save_id], uploaded.objects.keys())!233 }234 for id in uploaded.saves {235 strip_record(mut backend, save_key(repo, id), id, strips)!236 }237 drop_stripped_objects(store, mut backend, repo, uploaded, strips)!238 return true239}240241242243244fn strip_record(mut backend remote.Backend, key string, id string, strips []events.Event) ! {245 for _ in 0 .. 5 {246 object := backend.get(key)!247 if !object.found {248 return249 }250 record := decode_save(object.body)!251 kept := record.files.filter(fn [strips, id] (file FileRecord) bool {252 return !strips.any(reaches(it, id) && covers(it.path, file.path))253 })254 if kept.len == record.files.len255 || backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), object.tag)! {256 return257 }258 }259 return error('the remote copy of save ${id} kept changing while a stripped path was removed; run koh doctor again')260}261262263264pub struct Partway {265 Error266pub:267 done string268 reason string269}270271pub fn (e Partway) msg() string {272 return e.reason273}274275276277pub fn partway(done []string, failure IError) IError {278 mut all := done.clone()279 if failure is Partway {280 all << failure.done281 }282 if all.len == 0 {283 return failure284 }285 return Partway{286 done: all.join('; ')287 reason: failure.msg()288 }289}290291292293294fn drop_stripped_objects(store &storage.Store, mut backend remote.Backend, repo string, uploaded Uploaded, strips []events.Event) ! {295 mut reached := map[string]bool{}296 for save in history.all_saves(store)! {297 for file in history.files_of(store, save.id)! {298 if file.hash in uploaded.objects && strips.any(reaches(it, save.id) && covers(it.path, file.path)) {299 reached[file.hash] = true300 }301 }302 }303 if reached.len == 0 {304 return305 }306 for key in backend.list('${repo}/saves')! {307 object := backend.get(key)!308 if object.found {309 record := decode_save(object.body)!310 for file in record.files {311 if !strips.any(reaches(it, record.id) && covers(it.path, file.path)) {312 reached.delete(file.hash)313 }314 }315 }316 }317 for hash, _ in reached {318 backend.delete(object_key(repo, hash))!319 }320}321322323324325fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport, mut uploaded Uploaded) ! {326 files := history.files_of(store, save.id)!327 328 329 330 mut sent := map[string]bool{}331 if save.parent != '' {332 for file in history.files_of(store, save.parent)! {333 sent[file.hash] = true334 }335 }336 for file in files {337 if file.hash in sent {338 continue339 }340 sent[file.hash] = true341 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {342 report.objects++343 uploaded.objects[file.hash] = true344 }345 }346 record := save_record(store, save)!347 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {348 report.saves++349 uploaded.saves << save.id350 }351}352353354355fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {356 already := backend.list(events_prefix(repo))!357 mut already_sent := map[string]bool{}358 for key in already {359 already_sent[os.base(key)] = true360 }361 for event in events.all(store)! {362 if event.id in already_sent {363 continue364 }365 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {366 report.events++367 }368 }369}370371372373374pub struct Fetched {375pub mut:376 repo_objects map[string][]u8377 saves []SaveRecord 378 events_out []events.Event379 head string380 base string 381 unsafe_paths []string382}383384385386387pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {388 check_format(mut backend, repo)!389 remote_head := backend.get(head_key(repo))!390 if !remote_head.found {391 return error('the remote has no "${repo}" yet — run koh offer here first')392 }393 mut fetched := Fetched{394 head: remote_head.body.bytestr().trim_space()395 }396 mut on_line := map[string]bool{}397 for save in history.line(store)! {398 on_line[save.id] = true399 }400 401 fetched.events_out = fetch_events(store, mut backend, repo)!402 strips := known_strips(store, fetched)!403 mut cursor := fetched.head404 for cursor != '' && cursor !in on_line {405 mut record := if saved := history.by_id(store, cursor) {406 save_record(store, saved)!407 } else {408 record_object := backend.get(save_key(repo, cursor))!409 if !record_object.found {410 return error('the remote is missing save ${cursor} — run koh offer there')411 }412 decode_save(record_object.body)!413 }414 fetched.unsafe_paths << drop_unsafe_paths(mut record)415 for file in record.files {416 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects417 && !strips.any(reaches(it, record.id) && covers(it.path, file.path)) {418 blob := backend.get(object_key(repo, file.hash))!419 if !blob.found {420 return error('the remote is missing object ${file.hash} that save ${record.id} needs')421 }422 fetched.repo_objects[file.hash] = blob.body423 }424 }425 fetched.saves << record426 cursor = record.parent427 }428 fetched.saves.reverse_in_place()429 fetched.base = cursor430 return fetched431}432433fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {434 have := events.all(store)!435 mut known := map[string]bool{}436 for event in have {437 known[event.id] = true438 }439 mut incoming := []events.Event{}440 for key in backend.list(events_prefix(repo))! {441 id := os.base(key)442 if id in known {443 continue444 }445 body := backend.get(event_key(repo, id))!446 if !body.found {447 continue448 }449 event := decode_event(body.body)!450 if !ignored_strip(event) {451 incoming << event452 }453 }454 return incoming455}456457458459460pub fn write_fetched(store &storage.Store, fetched Fetched) ! {461 for hash, blob in fetched.repo_objects {462 store.put_object_blob(hash, blob)!463 }464 mut stripped := false465 for event in fetched.events_out {466 if !events.receive(store, event)! {467 continue468 }469 if events.valid_strip(event) {470 remove_path(store, event.path, event.save_id)!471 stripped = true472 }473 }474 strips := known_strips(store, fetched)!475 for record in fetched.saves {476 import_arriving(store, record, strips)!477 }478 if stripped {479 store.drop_unreferenced_objects()!480 }481}482483484485pub fn write_fetched_records(store &storage.Store, fetched Fetched) ! {486 for hash, blob in fetched.repo_objects {487 store.put_object_blob(hash, blob)!488 }489 strips := known_strips(store, fetched)!490 for record in fetched.saves {491 import_arriving(store, record, strips)!492 }493}494495496497fn known_strips(store &storage.Store, fetched Fetched) ![]events.Event {498 mut strips := events.strip_events(store)!499 strips << fetched.events_out500 return strips.filter(events.valid_strip(it))501}502503504505fn import_arriving(store &storage.Store, record SaveRecord, strips []events.Event) ! {506 if history.by_id(store, record.id) != none {507 return508 }509 files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {510 return snapshot.legal_path(file.path)511 && !strips.any(reaches(it, record.id) && covers(it.path, file.path))512 })513 history.import_save(store, to_save(record), files)!514}515516517518fn reaches(strip events.Event, save_id string) bool {519 return strip.save_id == '' || strip.save_id == save_id520}