Skip to main content

coven_database/
file_pins.rs

1//! The files a switch to the cloud asked this device to keep whole (§17.8):
2//! a durable request, recorded in the switching write, that coven fulfils
3//! itself, resuming after a restart. An app file's request keeps coven's copy
4//! until the cache copy is made from it; a user's original is never read for
5//! the cache, so its request downloads from storage.
6
7use crate::{sqlite::DatabaseConnection, DbError};
8use coven_foundation::{files::StagedName, id_source::FileId};
9use rusqlite::params;
10
11/// One file this device keeps whole as a switch asked.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct PinRequest {
14    /// The request's own id, never reused: its wait, and what coven keeps
15    /// of it in memory, are never a later request's for the same file.
16    pub id: i64,
17    /// The file.
18    pub file: FileId,
19    /// Coven's copy of an app file's bytes, while the cache copy is still to
20    /// be made from it; `None` for a file the pin downloads from storage.
21    pub copy: Option<StagedName>,
22    /// The persisted wait after a failed attempt, if one is armed (§6.7).
23    pub retry: Option<crate::RetryWait>,
24}
25
26/// Ask this device to keep `file` whole, from coven's copy when it has one.
27pub(crate) fn request(db: &DatabaseConnection, file: FileId) -> Result<(), DbError> {
28    db.internal_execute(
29        "INSERT INTO _coven_pin_requests(file,copy)
30         SELECT ?1,(SELECT path FROM _coven_device_files WHERE file=?1)
31         ON CONFLICT DO NOTHING",
32        params![file],
33    )?;
34    Ok(())
35}
36
37/// End `file`'s request, if one waits: the app unpinned the file, or it is
38/// gone from this device or left its stored bytes. Returns whether a request
39/// waited.
40pub(crate) fn end(db: &DatabaseConnection, file: FileId) -> Result<bool, DbError> {
41    let ended =
42        db.query("DELETE FROM _coven_pin_requests WHERE file=?1 RETURNING copy", [file], |r| {
43            r.get::<_, Option<String>>(0)
44        })?;
45    release(db, ended)
46}
47
48/// End the request `id`, if it still waits: its pin is made, or its file
49/// left the stored bytes. Returns whether it waited.
50pub(crate) fn end_request(db: &DatabaseConnection, id: i64) -> Result<bool, DbError> {
51    let ended =
52        db.query("DELETE FROM _coven_pin_requests WHERE id=?1 RETURNING copy", [id], |r| {
53            r.get::<_, Option<String>>(0)
54        })?;
55    release(db, ended)
56}
57
58/// An ended request's record and wait went with its row; the copy it kept
59/// goes once nothing else names it. Returns whether a request ended.
60fn release(db: &DatabaseConnection, mut ended: Vec<Option<String>>) -> Result<bool, DbError> {
61    let Some(copy) = ended.pop() else {
62        return Ok(false);
63    };
64    if let Some(copy) = copy {
65        crate::file_queue::release_copy(db, &copy)?;
66    }
67    Ok(true)
68}
69
70/// The cache copy couldn't be made from coven's copy for request `id`: the
71/// pin downloads from storage instead, and the copy goes once nothing else
72/// names it.
73pub(crate) fn drop_copy(db: &DatabaseConnection, id: i64) -> Result<(), DbError> {
74    let copy = db
75        .query("SELECT copy FROM _coven_pin_requests WHERE id=?1", [id], |r| {
76            r.get::<_, Option<String>>(0)
77        })?
78        .pop()
79        .flatten();
80    let Some(copy) = copy else {
81        return Ok(());
82    };
83    db.internal_execute("UPDATE _coven_pin_requests SET copy=NULL WHERE id=?1", [id])?;
84    crate::file_queue::release_copy(db, &copy)
85}
86
87/// Every request, with its persisted wait.
88pub(crate) fn read(db: &DatabaseConnection) -> Result<Vec<PinRequest>, DbError> {
89    db.query("SELECT id,file,copy FROM _coven_pin_requests ORDER BY file", [], |r| {
90        Ok((r.get::<_, i64>(0)?, r.get::<_, FileId>(1)?, r.get::<_, Option<String>>(2)?))
91    })?
92    .into_iter()
93    .map(|(id, file, copy)| {
94        Ok(PinRequest {
95            id,
96            file,
97            copy: copy
98                .map(|copy| StagedName::parse(&copy).ok_or(DbError::DamagedDatabase))
99                .transpose()?,
100            retry: crate::retry::find(db, crate::RetrySubject::Pin(id))?,
101        })
102    })
103    .collect()
104}
105
106#[cfg(test)]
107#[path = "file_pins_tests.rs"]
108pub(crate) mod tests;