//! 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{} 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}') } for file in record.files { if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects { 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 sent := 0 for hash in hashes { key := object_key(repo, hash) existing := backend.get(key)! if existing.found { data := zlib.decompress(existing.body) or { []u8{} } // Another machine may have put it right since the audit. if storage.hash_hex(data) == hash { continue } backend.delete(key)! } store.get_object(hash)! if backend.put_if_absent(key, store.object_blob(hash)!)! { sent++ } } 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)!)! { sent++ } } if backend.put_if_absent(save_key(repo, id), encode_save(save_record(store, save)!).bytes())! { sent++ } } return sent } /// 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()! mut removed := 0 for event in events.strip_events(store) or { store.rollback() return err } { removed += remove_path(store, event.path, event.save_id) or { store.rollback() return err } } store.drop_unreferenced_objects() or { store.rollback() return err } store.end()! return removed }