//! Doctor's repairs that copy records between this repo and its remote. //! //! Each one only puts back what another copy of the repo verifiably holds; //! none changes what a save contains. module exchange import compress.zlib import src.events import src.history import src.remote import src.storage /// Fetch saves this repo is missing, with any missing ancestors and objects, /// in one transaction. pub fn fetch_missing(store &storage.Store, mut backend remote.Backend, repo string, ids []string) !int { mut fetched := Fetched{} strips := known_strips(store, fetched)! mut wanted := ids.clone() mut seen := map[string]bool{} for wanted.len > 0 { id := wanted.pop() if id == '' || id in seen || history.by_id(store, id) != none { continue } seen[id] = true object := backend.get(save_key(repo, id))! if !object.found { return error('the remote does not have save ${id} either') } record := decode_save(object.body)! if record.id != id { return error('remote save ${id} holds the record of ${record.id}') } // The same scalar rules Doctor checks locally: an invalid record is // refused here, not imported for Doctor to report afterwards. if record.created_at < 0 || record.size_bytes < 0 { return error('remote save ${id} has invalid metadata') } for file in record.files { if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects && !strips.any(reaches(it, id) && covers(it.path, file.path)) { blob := backend.get(object_key(repo, file.hash))! if !blob.found { return error('the remote is missing object ${file.hash} that save ${id} needs') } fetched.repo_objects[file.hash] = blob.body } } fetched.saves << record wanted << record.parent wanted << record.restores } // The saves table refers to each parent, so parents land first. mut ordered := []SaveRecord{} mut placed := map[string]bool{} for ordered.len < fetched.saves.len { before := ordered.len for record in fetched.saves { if record.id !in placed && (record.parent !in seen || record.parent in placed) { ordered << record placed[record.id] = true } } if ordered.len == before { return error('the remote history has a parent cycle') } } fetched.saves = ordered store.begin()! write_fetched_records(store, fetched) or { store.rollback() return err } store.end()! return ordered.len } /// Upload verified local copies of remote objects that are missing or damaged, /// and of missing save records with their objects. pub fn resend(store &storage.Store, mut backend remote.Backend, repo string, hashes []string, save_ids []string) !int { // A record sent before this repo applies a remote strip would bring the // stripped path back. if unapplied_strips(store, mut backend, repo)!.len > 0 { return error(strip_not_applied) } mut uploaded := Uploaded{} upload_copies(store, mut backend, repo, hashes, save_ids, mut uploaded) or { return partway(uploaded.done(), after_failed_upload(store, mut backend, repo, uploaded, err)) } // As in offer: a strip that landed during the uploads may have missed them. if undo_late_strips(store, mut backend, repo, uploaded) or { return partway(uploaded.done(), err) } { return partway(uploaded.done(), error(strip_not_applied)) } return uploaded.objects.len + uploaded.saves.len } fn upload_copies(store &storage.Store, mut backend remote.Backend, repo string, hashes []string, save_ids []string, mut uploaded Uploaded) ! { for hash in hashes { key := object_key(repo, hash) existing := backend.get(key)! if existing.found { // Another machine may have put it right since the audit. Bytes that do // not decompress are damaged, even where empty bytes would hash right. if data := zlib.decompress(existing.body) { if storage.hash_hex(data) == hash { continue } } backend.delete(key)! } store.get_object(hash)! if backend.put_if_absent(key, store.object_blob(hash)!)! { uploaded.objects[hash] = true } } for id in save_ids { save := history.by_id(store, id) or { return error('save ${id} is missing here too') } for file in history.files_of(store, id)! { store.get_object(file.hash)! if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! { uploaded.objects[file.hash] = true } } if backend.put_if_absent(save_key(repo, id), encode_save(save_record(store, save)!).bytes())! { uploaded.saves << id } } } /// Delete what an upload sent before a late strip it could not settle: each /// object no remote record lists that this repo, with every remote strip /// applied, neither holds nor names in a save. pub fn settle_unsettled(store &storage.Store, mut backend remote.Backend, repo string) ! { if unapplied_strips(store, mut backend, repo)!.len > 0 { return error(strip_not_applied) } mut needed := map[string]bool{} for key in backend.list('${repo}/saves')! { object := backend.get(key)! if object.found { for file in decode_save(object.body)!.files { needed[file.hash] = true } } } mut deleted := 0 for hash in unsettled(store)! { if hash !in needed && storage.valid_hash(hash) && !store.has_object(hash)! && store.paths_of_object(hash)!.len == 0 { backend.delete(object_key(repo, hash)) or { return partway(done_text('deleted', deleted, 'remote object'), err) } deleted++ } } store.remove_state(unsettled_key) or { return partway(done_text('deleted', deleted, 'remote object'), err) } } /// Remove what recorded strips cover but a save still holds, without /// recording a new strip. pub fn reapply_strips(store &storage.Store) !int { store.begin()! removed := reapply_valid_strips(store) or { store.rollback() return err } store.end()! return removed } // An invalid strip event is reported, never applied, and only the objects a // strip frees go with it. fn reapply_valid_strips(store &storage.Store) !int { mut removed := 0 for event in events.strip_events(store)!.filter(events.valid_strip(it)) { hashes := stripped_hashes(store, event.path, event.save_id)! removed += remove_path(store, event.path, event.save_id)! store.drop_unused(hashes)! } return removed }