Skip to main content

gonzalo_core/
undelete.rs

1//! Restore a record from its tombstone, while the tombstone still exists
2//! (ADR 0029).
3//!
4//! Only the kinds whose body a tombstone retains can be restored — the manifest
5//! kinds. Every other tombstone has an empty body, so there is nothing to
6//! restore from.
7//!
8//! This rides [`Store::put_raw`] rather than `put`. A consumer `put` treats a
9//! tombstone as absent and re-stamps `meta.created`, which is right for `put` —
10//! it cannot tell restoring a deleted record from reusing a recycled key, and
11//! must not change meaning depending on whether `collect` has run. `put_raw`
12//! never re-stamps, so an explicit restore keeps the record's age. "Raw" means a
13//! record that already happened, and a restoration is exactly that.
14
15use crate::{
16    BlobStore, ContentHash, CoreError, Identity, Manifest, PutResult, Record, RecordKey,
17    RecordKind, Result, Revision, Store, VectorManifest,
18};
19
20/// Restore the record at `key` from its tombstone, returning the revision
21/// written. `author`, when given, is stamped as the restorer; `None` keeps the
22/// tombstone's author.
23///
24/// The body, kind and `meta` come back; `links` do not, because a tombstone
25/// never kept them.
26///
27/// Refuses, without writing anything, when there is no tombstone, the record is
28/// live, the tombstone's body was not retained, a blob the body names is
29/// missing, or the key changed while this ran.
30pub async fn undelete<S>(
31    store: &S,
32    key: &RecordKey,
33    now_ms: i64,
34    author: Option<&Identity>,
35) -> Result<Revision>
36where
37    S: Store + BlobStore + ?Sized,
38{
39    let Some(tomb) = store.get_raw(key).await? else {
40        return Err(CoreError::NotFound(key.clone()));
41    };
42    if !tomb.is_tombstone() {
43        return Err(CoreError::Invalid(format!(
44            "{key} is live at revision {}; there is nothing to restore",
45            tomb.revision.counter
46        )));
47    }
48    let Some(kind) = tomb.deleted_kind else {
49        return Err(CoreError::Invalid(format!(
50            "{key}: the deleted body was not retained, so it cannot be restored. \
51             Only manifest kinds retain a body (ADR 0029)."
52        )));
53    };
54
55    // Check the blobs before writing: a record for an index that cannot be
56    // opened is worse than a refusal.
57    let mut missing = Vec::new();
58    for hash in referenced_blobs(kind, &tomb)? {
59        if !store.has_blob(&hash).await? {
60            missing.push(hash.0);
61        }
62    }
63    if !missing.is_empty() {
64        return Err(CoreError::Invalid(format!(
65            "{key}: cannot restore, {} blob(s) the retained body names are gone: {}",
66            missing.len(),
67            missing.join(", ")
68        )));
69    }
70
71    let mut meta = tomb.meta.clone();
72    if let Some(author) = author {
73        meta.author = author.clone();
74    }
75    meta.updated = now_ms;
76
77    let restored = Record {
78        key: key.clone(),
79        kind,
80        revision: Revision {
81            counter: tomb.revision.counter + 1,
82            hash: ContentHash::of(tomb.body.bytes()),
83        },
84        parent: Some(tomb.revision.clone()),
85        body: tomb.body.clone(),
86        meta,
87        links: Vec::new(),
88        // `plan_put_raw` folds the tombstone in.
89        ancestors: Vec::new(),
90        deleted_at: None,
91        deleted_blob: None,
92        deleted_kind: None,
93    };
94
95    match store.put_raw(restored, Some(tomb.revision.clone())).await? {
96        PutResult::Committed(rev) => Ok(rev),
97        PutResult::Conflict(c) => Err(CoreError::Invalid(format!(
98            "{key}: the key changed while restoring (now revision {}); nothing was written",
99            c.current.revision.counter
100        ))),
101    }
102}
103
104/// The blob hashes a retained body names.
105fn referenced_blobs(kind: RecordKind, tomb: &Record) -> Result<Vec<ContentHash>> {
106    Ok(match kind {
107        RecordKind::VectorManifest => VectorManifest::from_body(&tomb.body)?
108            .entries
109            .into_values()
110            .collect(),
111        RecordKind::GraphManifest => Manifest::from_body(&tomb.body)?
112            .entries
113            .into_values()
114            .collect(),
115        // `deleted_kind` is only ever set for retaining kinds, so this is
116        // unreachable through `tombstone_of`; a hand-written record could still
117        // get here, and it names no blobs.
118        _ => Vec::new(),
119    })
120}