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(©.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(©.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(©.temporary)?],
135 )? != 1
136 {
137 return Err(DbError::CopyPathTaken {
138 path: copy.path.clone(),
139 });
140 }
141 for folder in ©.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;