Skip to main content

coven/
handle_delete.rs

1//! Deleting a store from this device without losing unsent work (E1).
2
3use crate::*;
4use coven_foundation::files::StoreDirAccess;
5
6pub use coven_foundation::files::IfUnsent;
7
8impl CovenHandle {
9    /// The writes and files this device committed and has not uploaded, from
10    /// one committed snapshot: what deleting the store would lose. With storage
11    /// set up, `delete_store(IfUnsent::Refuse)` refuses with these counts.
12    /// Available while disconnected.
13    pub async fn unsent_work(&self) -> CovenResult<UnsentWork> {
14        self.database.unsent().await.map_err(CovenError::from)
15    }
16
17    /// The same pre-built query, live (E4).
18    pub fn subscribe_unsent_work(&self) -> LiveQuery<UnsentWork, CovenError> {
19        self.database.subscribe_unsent()
20    }
21
22    /// Deletes this store from this device. With storage set up, a location
23    /// recorded without credentials is refused with `SignInRequired`; then
24    /// unsent writes and files are refused unless `unsent` discards them;
25    /// both change nothing. The deletion then records itself in the store's
26    /// directory and publishes this device's removal (§14.1), waiting until
27    /// it applies. A failure before the removal is journaled removes the
28    /// record and is `Sync`, the store as it was. From the journal on the
29    /// deletion only goes forward: a failure closes the handle and is
30    /// `Unfinished`, and the store lists as being deleted, refuses opens and
31    /// is finished by `CovenBuilder::resume_deletion` (§11.6). Once the
32    /// removal applied, or with no storage set up, it closes this handle, a
33    /// failure there being `Closing`, and deletes as `Coven::discard_store`.
34    /// The call runs to its end even if the app drops its future. Only this
35    /// device leaves: the person's restore code and S3 key in iCloud
36    /// Keychain stay for their other devices (§13.1).
37    pub async fn delete_store(&self, unsent: IfUnsent) -> Result<(), StoreDeletionError> {
38        self.leave(unsent).await
39    }
40
41    /// Delete the store from this device and leave it, as `delete_store`
42    /// does, or finish a deletion recorded with `unsent`, in a task of its
43    /// own that runs to its end even if the caller stops waiting.
44    pub(crate) async fn leave(&self, unsent: IfUnsent) -> Result<(), StoreDeletionError> {
45        let handle = self.clone();
46        let bugs = self.database.bugs().clone();
47        crate::task::spawn_completing("delete the store", bugs, async move {
48            handle.leaving(unsent).await
49        })
50        .await?
51    }
52
53    async fn leaving(&self, unsent: IfUnsent) -> Result<(), StoreDeletionError> {
54        if let Some(location) = self.codes.location().await? {
55            let stopped = self.database.device_stop().await.map_err(SyncError::from)?.is_some();
56            let journaled =
57                stopped || coven_sync::removal_journaled(&self.database, self.device).await?;
58            if self.codes.connection().await?.is_none() && !stopped {
59                if !journaled {
60                    self.forget_deletion().await?;
61                }
62                return Err(StoreDeletionError::SignInRequired(location.provider()));
63            }
64            if !journaled {
65                let work = self.database.unsent().await.map_err(SyncError::from)?;
66                if unsent == IfUnsent::Refuse && !work.is_empty() {
67                    self.forget_deletion().await?;
68                    return Err(StoreDeletionError::Unsent(work));
69                }
70                #[cfg(test)]
71                let gate = self.storage.deletion_gate();
72                self.codes
73                    .while_open("record the deletion", move |directory| {
74                        #[cfg(test)]
75                        if let Some(gate) = gate {
76                            gate();
77                        }
78                        directory.record_deletion(unsent)
79                    })
80                    .await?;
81            }
82            // A device stopped for good has nothing left to remove.
83            if !stopped {
84                let retired = async {
85                    self.storage.connected().await?;
86                    self.storage.retire().await
87                }
88                .await;
89                if let Err(error) = retired {
90                    return Err(self.retiring_failed(error).await);
91                }
92            }
93        }
94        self.close().await.map_err(StoreDeletionError::Closing)?;
95        let directory = self.directory.clone();
96        let keychain = self.keychain.clone();
97        let bugs = self.database.bugs().clone();
98        crate::task::blocking_completing("delete the store", bugs, move || {
99            crate::coven::delete::<StoreDeletionError>(&directory, &keychain)
100        })
101        .await?
102    }
103
104    /// The deletion failed connecting or publishing the removal. Before the
105    /// removal is journaled nothing in storage changed, so the record goes
106    /// and the store is as it was. From the journal on it only goes forward:
107    /// the handle closes, and the record stays for `resume_deletion`.
108    async fn retiring_failed(&self, error: SyncError) -> StoreDeletionError {
109        let journaled = async {
110            Ok::<_, SyncError>(
111                self.database.device_stop().await?.is_some()
112                    || coven_sync::removal_journaled(&self.database, self.device).await?,
113            )
114        }
115        .await;
116        match journaled {
117            Ok(false) => match self.forget_deletion().await {
118                Ok(()) => StoreDeletionError::from(error),
119                Err(cleanup) => StoreDeletionError::from(SyncError::cleanup(error, cleanup)),
120            },
121            Ok(true) => StoreDeletionError::Unfinished(match self.close().await {
122                Ok(()) => error,
123                Err(cleanup) => SyncError::cleanup(error, cleanup),
124            }),
125            // Whether the removal was journaled can't be read: the record
126            // stays, and the store lists as being deleted.
127            Err(read) => StoreDeletionError::Unfinished(SyncError::cleanup(error, read)),
128        }
129    }
130
131    /// Remove the deletion's record: it ended before its removal was
132    /// journaled. An absent record succeeds.
133    async fn forget_deletion(&self) -> Result<(), SyncError> {
134        let directory = self.directory.clone();
135        let bugs = self.database.bugs().clone();
136        crate::task::blocking_completing("forget the deletion", bugs, move || {
137            directory.forget_deletion()
138        })
139        .await
140        .map_err(SyncError::from)?
141        .map_err(SyncError::from)
142    }
143}
144
145#[cfg(test)]
146#[path = "handle_delete_tests.rs"]
147mod tests;