Skip to main content

coven_database/
file_database_copies.rs

1//! The records behind making cloud files device-only (§17.3): who may, the
2//! copies a call writes into the person's folder, recorded before any of
3//! their bytes exist, the write switching the call's files to them, and the
4//! removal of what a failing call made.
5
6use super::{BytesWriting, Database, FileDatabase};
7use crate::{sqlite::DatabaseConnection, DbError, DeviceOnlyCopy, FileRef};
8use coven_foundation::locks::lock_unpoisoned;
9use coven_foundation::task::{BugSinkRef, CarriesBug, TaskFailed};
10use coven_foundation::{
11    files::{Flushed, ObservedFile},
12    id_source::FileId,
13};
14use rusqlite::params;
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, Mutex};
17
18/// One copy a call to make files device-only writes (§17.3): its path, the
19/// temporary name in the same folder its bytes are written under, and the
20/// folders the call makes on its path.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct FolderCopy {
23    /// Where the copy goes.
24    pub path: PathBuf,
25    /// The name its bytes are written under until they are whole and synced.
26    pub temporary: PathBuf,
27    /// The folders the call makes that hold it, outermost first.
28    pub folders: Vec<PathBuf>,
29}
30
31impl FileDatabase {
32    /// Each file as its record stands, once this device may make it
33    /// device-only (§17.3): in the cloud, of a user namespace, in an
34    /// audience this device's member is in, and referenced by no row of an
35    /// `owner_only` table another member created. Read in one committed
36    /// state; the write switching the files checks again.
37    pub async fn device_only_files(&self, files: Vec<FileId>) -> Result<Vec<FileRef>, DbError> {
38        self.database
39            .call(move |inner| {
40                inner.access.with_committed(|db| {
41                    files
42                        .iter()
43                        .map(|file| {
44                            crate::file_write::permitted(
45                                db,
46                                &inner.access.write_schema,
47                                inner.access.device,
48                                *file,
49                            )
50                            .and_then(FileRef::of)
51                        })
52                        .collect()
53                })
54            })
55            .await
56    }
57
58    /// Record the copies one call writes, before any of their folders or
59    /// bytes exist (`Database::record_copies`). A path another call is
60    /// writing a copy at is `CopyPathTaken`; the returned reservation keeps
61    /// the copies from other writes' removals while it is held.
62    pub async fn record_copies(&self, copies: Vec<FolderCopy>) -> Result<CopyReservation, DbError> {
63        self.database.record_copies(copies).await
64    }
65
66    /// The call's own mkdir made `folder`: it is the call's to remove, once
67    /// empty. A folder that was there already is never marked.
68    pub async fn made_folder(&self, folder: &Path) -> Result<(), DbError> {
69        self.database.made_folder(folder).await
70    }
71
72    /// A copy is whole and synced at its path, renamed there from its
73    /// temporary name, with the size and modification time `observed` read.
74    pub async fn place_copy(&self, observed: &ObservedFile) -> Result<(), DbError> {
75        self.database.place_copy(observed).await
76    }
77
78    /// The copies a call placed, ready for the write that switches their
79    /// files to them (§17.3). It takes the flush of the batch the copies
80    /// were written in: they are on the drive before the commit that records
81    /// them (Appendix C.3).
82    pub fn prepared_device_only(
83        &self,
84        mut reservation: CopyReservation,
85        copies: Vec<DeviceOnlyCopy>,
86        _flushed: Flushed,
87    ) -> Result<PreparedDeviceOnly, DbError> {
88        // The copies stay reserved, now for the prepared call, which holds
89        // nothing close or a reopen waits for.
90        let _writing = reservation.writing.take();
91        let database = &self.database;
92        Ok(PreparedDeviceOnly { database: database.clone(), reservation, copies })
93    }
94
95    /// Remove what a failing call made: the copies at `paths` it left
96    /// unswitched, if they are as it left them, and the folders it made for
97    /// them, if they are empty. A failed removal keeps its record, and the
98    /// next open removes it.
99    pub async fn abandon_copies(&self, paths: Vec<PathBuf>) -> Result<(), DbError> {
100        let paths = paths
101            .iter()
102            .map(|path| text(path).map(str::to_owned))
103            .collect::<Result<Vec<_>, _>>()?;
104        self.database.abandon_copies(paths).await
105    }
106}
107
108impl Database {
109    /// Record the copies one call writes, before any of their folders or
110    /// bytes exist, so a failure or a crash leaves nothing unrecorded. A path
111    /// another call is writing a copy at is `CopyPathTaken`. While the
112    /// returned reservation is held, no other write's removal takes the
113    /// copies; once it drops without the call's switch, as when the call's
114    /// future is dropped, the next write removes them (Appendix C.3). Until
115    /// the copies are prepared or it drops, closing the store waits for it,
116    /// and so does a reopen once the last handle drops.
117    pub(crate) async fn record_copies(
118        &self,
119        copies: Vec<FolderCopy>,
120    ) -> Result<CopyReservation, DbError> {
121        let paths = copies
122            .iter()
123            .map(|copy| text(&copy.path).map(str::to_owned))
124            .collect::<Result<Vec<_>, _>>()?;
125        let writing = BytesWriting::of(self).await;
126        self.call(move |inner| {
127            inner.access.with_writer(|writer| {
128                writer.transaction(|db: &DatabaseConnection| {
129                    for copy in &copies {
130                        let path = text(&copy.path)?;
131                        if db.internal_execute(
132                            "INSERT INTO _coven_copies(path,temporary,state) VALUES(?1,?2,'writing')
133                             ON CONFLICT(path) DO NOTHING",
134                            params![path, text(&copy.temporary)?],
135                        )? != 1
136                        {
137                            return Err(DbError::CopyPathTaken {
138                                path: copy.path.clone(),
139                            });
140                        }
141                        for folder in &copy.folders {
142                            db.internal_execute(
143                                "INSERT INTO _coven_copy_folders(folder,copy) VALUES(?1,?2)",
144                                params![text(folder)?, path],
145                            )?;
146                        }
147                    }
148                    Ok(())
149                })?;
150                // Registered under the writer, so no write's removal
151                // runs between the records and the registration.
152                lock_unpoisoned(&inner.copying).extend(paths.iter().cloned());
153                Ok::<_, DbError>(())
154            })?;
155            Ok(CopyReservation {
156                names: Arc::new(CopyNames {
157                    copying: inner.copying.clone(),
158                    paths: Mutex::new(paths),
159                }),
160                writing: Some(Arc::new(writing)),
161            })
162        })
163        .await
164    }
165
166    /// The call's own mkdir made `folder`: it is the call's to remove, once
167    /// empty. A folder that was there already is never marked.
168    pub(crate) async fn made_folder(&self, folder: &Path) -> Result<(), DbError> {
169        let folder = text(folder)?.to_owned();
170        self.call(move |inner| {
171            inner.access.with_writer(|writer| {
172                writer.transaction(|db: &DatabaseConnection| {
173                    db.internal_execute(
174                        "UPDATE _coven_copy_folders SET made=1 WHERE folder=?1",
175                        [&folder],
176                    )?;
177                    Ok(())
178                })
179            })
180        })
181        .await
182    }
183
184    /// A copy is whole and synced at its path, renamed there from its
185    /// temporary name, with the size and modification time `observed` read.
186    pub(crate) async fn place_copy(&self, observed: &ObservedFile) -> Result<(), DbError> {
187        let path = text(observed.path())?.to_owned();
188        let size = crate::sqlite::integer("file size", observed.size())?;
189        let modified_at = crate::user_file::encode_time(observed.modified_at());
190        self.call(move |inner| {
191            inner.access.with_writer(|writer| {
192                writer.transaction(|db: &DatabaseConnection| {
193                    if db.internal_execute(
194                        "UPDATE _coven_copies SET state='placed',size=?2,modified_at=?3
195                         WHERE path=?1 AND state='writing'",
196                        params![path, size, modified_at],
197                    )? != 1
198                    {
199                        return Err(DbError::Invariant {
200                            what: "a copy placed is one its call is writing",
201                        });
202                    }
203                    Ok(())
204                })
205            })
206        })
207        .await
208    }
209
210    /// Remove what a failing call made: the copies at `paths` it left
211    /// unswitched, if they are as it left them, and the folders it made for
212    /// them, if they are empty, once the writer is free. A failed removal
213    /// keeps its record, and the next open removes it.
214    pub(crate) async fn abandon_copies(&self, paths: Vec<String>) -> Result<(), DbError> {
215        self.call(move |inner| {
216            let chosen = inner.access.with_writer(|writer| {
217                Ok::<_, DbError>(crate::file_removals::choose_copies(writer, &paths))
218            })?;
219            inner.finish_removals(Ok::<_, DbError>(()), chosen)
220        })
221        .await
222    }
223}
224
225/// Cloud files downloaded to copies in the person's folder, ready for one
226/// write to make them device-only here (§17.3, E8). It holds nothing close
227/// waits for. Committed by no write, its copies stay reserved while it is
228/// held, and the next write or open removes them once it drops; `abandon`
229/// removes them at once.
230pub struct PreparedDeviceOnly {
231    database: Database,
232    reservation: CopyReservation,
233    copies: Vec<DeviceOnlyCopy>,
234}
235
236impl PreparedDeviceOnly {
237    /// The copies' paths, as the store reserving them records them.
238    pub(crate) fn paths(&self) -> Vec<String> {
239        lock_unpoisoned(&self.reservation.names.paths).clone()
240    }
241
242    /// The switches, and the copies' paths, for the write making them;
243    /// the reservation's release is the write's from here.
244    pub(crate) fn take(mut self) -> (Vec<DeviceOnlyCopy>, Vec<String>) {
245        let paths = std::mem::take(&mut *lock_unpoisoned(&self.reservation.names.paths));
246        (std::mem::take(&mut self.copies), paths)
247    }
248
249    /// Remove the copies, and the folders made for them, now, instead of
250    /// at the next write or open (§17.3). A failed removal keeps its record,
251    /// and the next open removes it.
252    pub async fn abandon(self) -> Result<(), DbError> {
253        let paths = self.paths();
254        let database = self.database.clone();
255        drop(self);
256        database.abandon_copies(paths).await
257    }
258}
259
260impl std::fmt::Debug for PreparedDeviceOnly {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        f.debug_struct("PreparedDeviceOnly")
263            .field("copies", &self.copies.len())
264            .finish_non_exhaustive()
265    }
266}
267
268/// The copies one call to make files device-only is writing, which no other
269/// write's removal takes while it is held or a step it runs is under way.
270/// Once both end, whatever became of the call, the next write removes those
271/// its switch didn't keep.
272pub struct CopyReservation {
273    // The names go before the store the writing holds can close.
274    names: Arc<CopyNames>,
275    /// Held while the copies are written, until they are prepared.
276    writing: Option<Arc<BytesWriting>>,
277}
278
279impl CopyReservation {
280    /// Run `work`, a step of the call named `operation` that makes a name in
281    /// the person's folder, as its own task, holding the call's copies, so
282    /// it runs to its end with its record though the call is dropped
283    /// meanwhile: until it ends, no write's removal takes the call's copies,
284    /// and closing the store waits for it (§17.3). Its panic comes back to a
285    /// caller still waiting, and goes to `bugs` once none is (E15).
286    pub async fn run_step<T: Send + 'static, E: CarriesBug + Send + 'static>(
287        &self,
288        operation: &'static str,
289        bugs: BugSinkRef,
290        work: impl std::future::Future<Output = Result<T, E>> + Send + 'static,
291    ) -> Result<Result<T, E>, TaskFailed> {
292        // The names go before the store the writing holds can close.
293        let held = (self.names.clone(), self.writing.clone());
294        let stepped = coven_foundation::task::spawn_completing(operation, bugs, async move {
295            // Declared first, so it goes last as a panic unwinds the work.
296            let held = held;
297            // A value comes back with the hold, so whatever it keeps open,
298            // such as a copy's file, closes before the hold goes, also when
299            // no caller waits for it any more.
300            work.await.map(|value| (value, held))
301        })
302        .await?;
303        Ok(stepped.map(|(value, _held)| value))
304    }
305}
306
307/// The copies one call is writing, kept out of removals until the last of
308/// its reservation and the steps it runs drops.
309struct CopyNames {
310    /// The copies the store that recorded them keeps out of removals; a
311    /// closed store's records stay for its next open.
312    copying: Arc<Mutex<std::collections::BTreeSet<String>>>,
313    /// Until a write takes them to release once it has decided.
314    paths: Mutex<Vec<String>>,
315}
316
317impl Drop for CopyNames {
318    fn drop(&mut self) {
319        crate::database::release_copies(&self.copying, &lock_unpoisoned(&self.paths));
320    }
321}
322
323/// The call's copy at `path` is placed and its file switched to it: the
324/// copy is an original this device records (§17.3).
325pub(crate) fn keep(db: &DatabaseConnection, path: &str) -> Result<(), DbError> {
326    db.internal_execute("UPDATE _coven_copies SET state='kept' WHERE path=?1", [path])?;
327    Ok(())
328}
329
330/// The kept copy at the original's `path` lost its switch to another
331/// device's, unless another of this device's files is recorded there: it is
332/// recorded for removal (Appendix C.3).
333pub(crate) fn lose(db: &DatabaseConnection, path: &[u8], file: FileId) -> Result<(), DbError> {
334    db.internal_execute(
335        "UPDATE _coven_copies SET state='lost' WHERE path=CAST(?1 AS TEXT) AND state='kept'
336         AND NOT EXISTS(SELECT 1 FROM _coven_user_files WHERE path=?1 AND file!=?2)",
337        params![path, file],
338    )?;
339    Ok(())
340}
341
342/// The copy at `path`, recorded as `state`, is removed; its folder links go
343/// with it.
344pub(crate) fn forget(db: &DatabaseConnection, path: &str, state: &str) -> Result<(), DbError> {
345    db.internal_execute("DELETE FROM _coven_copies WHERE path=?1 AND state=?2", (path, state))?;
346    Ok(())
347}
348
349/// A path as coven records it: valid Unicode, as planning a copy checks.
350pub(crate) fn text(path: &Path) -> Result<&str, DbError> {
351    path.to_str().ok_or(DbError::Invariant { what: "a copy's path is valid Unicode" })
352}
353
354#[cfg(test)]
355#[path = "file_database_copies_tests.rs"]
356pub(crate) mod tests;