coven_database/
cache_progress.rs1use crate::{sqlite::DatabaseConnection, Database, DbError, FileRef, LiveQuery};
5use coven_foundation::id_source::{DeviceId, FileId};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct PinProgress {
10 pub files_completed: u64,
12 pub files_total: u64,
14 pub bytes_cached: u64,
17 pub bytes_total: u64,
19}
20
21#[expect(
25 clippy::map_err_ignore,
26 reason = "a stored value that won't convert or decode is DamagedDatabase"
27)]
28pub(crate) fn covered(
29 db: &DatabaseConnection,
30 file: &FileRef,
31 device: DeviceId,
32) -> Result<u64, DbError> {
33 match file.place() {
34 crate::FilePlace::DeviceOnly { device: on } if on == device => return Ok(file.size()),
35 crate::FilePlace::DeviceOnly { .. } => return Ok(0),
36 crate::FilePlace::Cloud { .. } => {}
37 }
38 let object = crate::database::file_database::object_path_of(file)?;
39 if db.query_row(
40 "SELECT EXISTS(SELECT 1 FROM _coven_cache
41 WHERE namespace=?1 AND object_path=?2 AND chunk=?3)",
42 (file.namespace(), &object, crate::internal_schema::CACHE_COMPLETE_FILE),
43 |r| r.get::<_, bool>(0),
44 )? {
45 return Ok(file.size());
46 }
47 let cached = db.query_row(
48 "SELECT coalesce(sum(size-chunks*?3),0) FROM _coven_cache
49 WHERE namespace=?1 AND object_path=?2 AND chunk>=0",
50 (file.namespace(), &object, coven_crypto::FILE_CHUNK_TAG_LEN as i64),
51 |r| r.get::<_, i64>(0),
52 )?;
53 let cached = u64::try_from(cached).map_err(|_| DbError::DamagedDatabase)?;
54 if cached > file.size() {
55 return Err(DbError::DamagedDatabase);
56 }
57 Ok(cached)
58}
59
60pub(crate) fn pin_progress(
63 db: &DatabaseConnection,
64 files: &[FileRef],
65 device: DeviceId,
66) -> Result<PinProgress, DbError> {
67 let mut progress = PinProgress {
68 files_completed: 0,
69 files_total: files.len() as u64,
70 bytes_cached: 0,
71 bytes_total: 0,
72 };
73 let too_large =
74 || DbError::TooLarge { field: "requested file bytes", actual: u64::MAX, maximum: u64::MAX };
75 for file in files {
76 let cached = covered(db, file, device)?;
77 progress.bytes_cached = progress.bytes_cached.checked_add(cached).ok_or_else(too_large)?;
78 progress.bytes_total =
79 progress.bytes_total.checked_add(file.size()).ok_or_else(too_large)?;
80 let whole = match file.place() {
81 crate::FilePlace::DeviceOnly { device: on } => on == device,
82 crate::FilePlace::Cloud { .. } => crate::database::file_database::cache_kept(db, file)?,
83 };
84 if whole {
85 progress.files_completed += 1;
86 }
87 }
88 Ok(progress)
89}
90
91pub(crate) fn files_pinned(
96 db: &DatabaseConnection,
97 files: &[FileId],
98) -> Result<Vec<Option<bool>>, DbError> {
99 files
100 .iter()
101 .map(|file| {
102 let (known, pinned): (bool, bool) = db.query_row(
103 "SELECT EXISTS(SELECT 1 FROM main.coven_files WHERE id=?1),
104 EXISTS(SELECT 1 FROM coven_pins WHERE file=?1)",
105 [file],
106 |r| Ok((r.get(0)?, r.get(1)?)),
107 )?;
108 Ok(known.then_some(pinned))
109 })
110 .collect()
111}
112
113impl Database {
114 pub async fn pin_progress(&self, files: Vec<FileRef>) -> Result<PinProgress, DbError> {
116 let device = self.local_device()?;
117 self.read(move |sql| sql.pin_progress(&files, device)).await
118 }
119 pub fn subscribe_pin_progress<E>(&self, files: Vec<FileRef>) -> LiveQuery<PinProgress, E>
121 where
122 E: From<DbError> + coven_foundation::task::CarriesBug + Send + 'static,
123 {
124 let device = self.local_device().ok();
125 self.subscribe(move |sql| {
126 sql.pin_progress(&files, device.ok_or(DbError::StoreClosed)?).map_err(E::from)
127 })
128 }
129 pub async fn files_pinned(&self, files: Vec<FileId>) -> Result<Vec<Option<bool>>, DbError> {
131 self.read(move |sql| sql.files_pinned(&files)).await
132 }
133 pub fn subscribe_files_pinned<E>(&self, files: Vec<FileId>) -> LiveQuery<Vec<Option<bool>>, E>
135 where
136 E: From<DbError> + coven_foundation::task::CarriesBug + Send + 'static,
137 {
138 self.subscribe(move |sql| sql.files_pinned(&files).map_err(E::from))
139 }
140}
141
142#[cfg(test)]
143#[path = "cache_progress_tests.rs"]
144mod tests;