Skip to main content

coven_database/
file_database_pins.rs

1//! The pins coven keeps for files kept here (§17.8), as the file owner
2//! reads and settles them.
3
4use super::{file_ref, FileDatabase};
5use crate::{DbError, FileRef};
6use coven_foundation::files::StagedName;
7
8impl FileDatabase {
9    /// The files a switch to the cloud asked this device to keep whole, each
10    /// with the copy its cache copy is made from and its wait (§17.8).
11    pub async fn pin_requests(&self) -> Result<Vec<crate::PinRequest>, DbError> {
12        self.read(crate::file_pins::read).await
13    }
14    /// The file of request `id` left the stored bytes the request asked to
15    /// keep: the request, its record and wait end, and the copy it kept goes
16    /// once nothing else names it.
17    pub async fn end_pin_request(&self, id: i64) -> Result<(), DbError> {
18        self.database
19            .call(move |inner| {
20                // The released copy goes once the writer is free (§21.2).
21                inner.with_files("end a pin request", |writer, _| {
22                    writer.state_transaction(|db| crate::file_pins::end_request(db, id).map(drop))
23                })
24            })
25            .await
26    }
27    /// Make the pin request `id` asked for from `prepared`, ending the
28    /// request in the same write, and return true (§17.8). A request that
29    /// ended since it was read, by the app's unpin or the file leaving,
30    /// makes nothing: false.
31    pub async fn keep(&self, prepared: crate::PreparedPin, id: i64) -> Result<bool, DbError> {
32        self.database
33            .write(move |sql| {
34                if !sql.end_pin_request(id)? {
35                    return Ok(false);
36                }
37                sql.pin(prepared)?;
38                Ok(true)
39            })
40            .await
41    }
42    /// The cache copy couldn't be made from coven's copy for request `id`:
43    /// the pin downloads from storage instead.
44    pub async fn drop_pin_copy(&self, id: i64) -> Result<(), DbError> {
45        self.database
46            .call(move |inner| {
47                inner.with_files("drop a pin's copy", |writer, _| {
48                    writer.transaction(|db| crate::file_pins::drop_copy(db, id))
49                })
50            })
51            .await
52    }
53    /// Record a failed attempt at the pin `request` asked for as its record,
54    /// with its next wait or none, in one transaction. A request already
55    /// ended records nothing.
56    pub async fn fail_pin<P: crate::PendingValue>(
57        &self,
58        request: &crate::PinRequest,
59        record: P,
60        retry: Option<crate::RetryWait>,
61    ) -> Result<(), DbError> {
62        let (id, file) = (request.id, request.file);
63        self.run(move |db, _, _, _| {
64            db.state_transaction(|db| {
65                if record.pin_of() != Some(file) {
66                    return Err(DbError::Invariant {
67                        what: "a kept pin's failure is its own record",
68                    });
69                }
70                if !db.query_row(
71                    "SELECT EXISTS(SELECT 1 FROM _coven_pin_requests WHERE id=?1)",
72                    [id],
73                    |r| r.get::<_, bool>(0),
74                )? {
75                    return Ok(());
76                }
77                crate::retry::save_failure(
78                    db,
79                    &record,
80                    crate::RetrySubject::Pin(id),
81                    retry.as_ref(),
82                )
83            })
84        })
85        .await
86    }
87    /// The first-read plaintext hash of one chunk of coven's copy, which the
88    /// cache copy made from it checks.
89    #[expect(
90        clippy::map_err_ignore,
91        reason = "a stored value that won't convert is damage, a computed one a broken invariant"
92    )]
93    pub async fn copy_chunk_hash(
94        &self,
95        copy: &StagedName,
96        index: u64,
97    ) -> Result<coven_crypto::ContentHash, DbError> {
98        let copy = copy.as_str().to_owned();
99        self.read(move |db| {
100            let index = i64::try_from(index)
101                .map_err(|_| DbError::Invariant { what: "a chunk index fits SQLite" })?;
102            let hash = db
103                .query(
104                    "SELECT hash FROM _coven_copy_chunks WHERE path=?1 AND chunk=?2",
105                    rusqlite::params![copy, index],
106                    |r| r.get::<_, Vec<u8>>(0),
107                )?
108                .pop()
109                .ok_or(DbError::DamagedDatabase)?;
110            crate::file_hashes::stored_hash(hash)
111        })
112        .await
113    }
114    /// Open coven's copy a pin request keeps, which must still have the
115    /// file's size; it touches only the disk.
116    pub async fn open_pin_copy(
117        &self,
118        file: &FileRef,
119        copy: &StagedName,
120    ) -> Result<crate::LocalFileStream, crate::LocalFileError> {
121        let (id, size, copy) = (file.id().to_string(), file.size(), copy.clone());
122        self.database
123            .call(move |inner| {
124                file_ref::open_source(
125                    &inner.access.directory,
126                    &id,
127                    size,
128                    file_ref::LocalSource::Copy(copy),
129                )
130            })
131            .await
132    }
133}