1use super::{cache, BytesWriting, CacheReservation, FileDatabase};
8use crate::{sqlite::DatabaseConnection, DbError, FileRef};
9use coven_foundation::files::FileName;
10
11impl FileDatabase {
12 #[expect(clippy::map_err_ignore, reason = "a stored name that won't decode is DamagedDatabase")]
18 pub async fn reserve_pin_download(
19 &self,
20 file: &FileRef,
21 fresh: FileName,
22 ) -> Result<(CacheReservation, u64), DbError> {
23 let database = &self.database;
24 let writing = BytesWriting::of(database).await;
25 let held = {
26 let file = file.clone();
27 database
28 .call(move |inner| {
29 let found = inner
30 .access
31 .with_writer(|writer| writer.read_transaction(|| partial(writer, &file)))?;
32 let Some((path, size)) = found else {
33 return Ok(None);
34 };
35 let name = FileName::new(&path).map_err(|_| DbError::DamagedDatabase)?;
36 inner.staging.reserve([crate::file_removals::OwnedFile::Cached(name.clone())]);
37 Ok::<_, DbError>(Some((
38 CacheReservation {
39 reserved: inner.staging.clone(),
40 file,
41 name,
42 writing: Some(writing),
43 partial: true,
44 },
45 size,
46 )))
47 })
48 .await?
49 };
50 match held {
51 Some(held) => Ok(held),
52 None => Ok((self.reserve_cache_file(file, fresh).await?, 0)),
53 }
54 }
55
56 #[expect(
62 clippy::map_err_ignore,
63 reason = "a size past SQLite's integer is TooLarge, which names the field and size"
64 )]
65 pub async fn keep_pin_download(
66 &self,
67 reservation: &mut CacheReservation,
68 size: u64,
69 ) -> Result<(), DbError> {
70 let (file, name, kept) =
71 (reservation.file.clone(), reservation.name.clone(), reservation.partial);
72 let size = i64::try_from(size).map_err(|_| DbError::TooLarge {
73 field: "partial pin download",
74 actual: size,
75 maximum: i64::MAX as u64,
76 })?;
77 self.run(move |db, _, _, _| db.transaction(|db| record(db, &file, &name, size, kept)))
78 .await?;
79 reservation.partial = true;
80 Ok(())
81 }
82
83 #[cfg(any(test, feature = "test-utils"))]
86 #[expect(clippy::map_err_ignore, reason = "a stored name that won't decode is DamagedDatabase")]
87 pub async fn pin_download_kept(
88 &self,
89 file: &FileRef,
90 ) -> Result<Option<(FileName, u64)>, DbError> {
91 let file = file.clone();
92 let found = self.read(move |db| partial(db, &file)).await?;
93 found
94 .map(|(path, size)| {
95 Ok((FileName::new(&path).map_err(|_| DbError::DamagedDatabase)?, size))
96 })
97 .transpose()
98 }
99
100 pub async fn discard_pin_download(&self, file: &FileRef) -> Result<(), DbError> {
103 let file = file.clone();
104 let chosen = self
105 .run(move |db, _, _, _| {
106 let object = cache::object_path_of(&file)?;
107 let paths = remove(db, file.namespace(), &object)?;
108 Ok::<_, DbError>(crate::file_removals::choose_cache(&paths))
109 })
110 .await?;
111 self.removed(chosen).await
112 }
113}
114
115#[expect(clippy::map_err_ignore, reason = "a stored size that won't convert is DamagedDatabase")]
118fn partial(db: &DatabaseConnection, file: &FileRef) -> Result<Option<(String, u64)>, DbError> {
119 let found = db
120 .query(
121 "SELECT path,size FROM _coven_cache_partials WHERE namespace=?1 AND object_path=?2",
122 (file.namespace(), cache::object_path_of(file)?),
123 |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)),
124 )?
125 .pop();
126 found
127 .map(|(path, size)| Ok((path, u64::try_from(size).map_err(|_| DbError::DamagedDatabase)?)))
128 .transpose()
129}
130
131fn record(
135 db: &DatabaseConnection,
136 file: &FileRef,
137 name: &FileName,
138 size: i64,
139 kept: bool,
140) -> Result<(), DbError> {
141 if !cache::named(db, file)? {
142 return Err(DbError::NoLongerStored { file: file.id() });
143 }
144 let object = cache::object_path_of(file)?;
145 if kept {
146 let updated = db.internal_execute(
147 "UPDATE _coven_cache_partials SET size=?4
148 WHERE namespace=?1 AND object_path=?2 AND path=?3",
149 rusqlite::params![file.namespace(), object, name.as_str(), size],
150 )?;
151 return if updated == 1 { Ok(()) } else { Err(DbError::PinChanged { file: file.id() }) };
152 }
153 crate::file_removals::keep(db, "cache", name.as_str())?;
155 db.internal_execute(
156 "INSERT INTO _coven_cache_partials(namespace,object_path,path,size) VALUES(?1,?2,?3,?4)",
157 rusqlite::params![file.namespace(), object, name.as_str(), size],
158 )?;
159 Ok(())
160}
161
162pub(crate) fn remove(
165 db: &DatabaseConnection,
166 namespace: &str,
167 object: &str,
168) -> Result<Vec<String>, DbError> {
169 let paths = db.query(
170 "DELETE FROM _coven_cache_partials WHERE namespace=?1 AND object_path=?2 RETURNING path",
171 (namespace, object),
172 |r| r.get::<_, String>(0),
173 )?;
174 for path in &paths {
175 crate::file_removals::record_cached(db, path)?;
176 }
177 Ok(paths)
178}