1use super::{Database, DatabaseAccess, DatabaseInner, Mode, Writer};
4use crate::internal_schema::CACHE_NAME_USED_SQL;
5use crate::DatabaseChanges;
6use crate::{
7 file_queue, file_ref, sqlite::DatabaseConnection, write_schema::WriteSchema, DbError, FileRef,
8 FileUpload,
9};
10use coven_crypto::SecretBytes;
11use coven_foundation::task::CarriesBug;
12use coven_foundation::{
13 files::{FileName, StoreDir},
14 id_source::{DeviceId, FileId, ObjectId},
15};
16use std::sync::Arc;
17
18#[path = "file_cache.rs"]
19pub(crate) mod cache;
20pub(crate) use cache::{
21 clear_entries as clear_cached, declare_budgets, kept as cache_kept, object_path,
22 object_path_of, publish as publish_cached, remove_entries as remove_cached,
23 set_budget as set_cache_budget, trim_entries as trim_cached,
24};
25
26pub struct FileDatabase<M: Mode = Writer> {
48 database: Database<M>,
49}
50
51impl<M: Mode> Clone for FileDatabase<M> {
53 fn clone(&self) -> Self {
54 Self { database: self.database.clone() }
55 }
56}
57
58impl<M: Mode> FileDatabase<M> {
59 pub fn new(database: Database<M>) -> Self {
61 Self { database }
62 }
63
64 pub fn bugs(&self) -> &coven_foundation::task::BugSinkRef {
67 self.database.bugs()
68 }
69
70 pub fn records(&self) -> Option<&FileDatabase<Writer>> {
73 M::records(self)
74 }
75
76 async fn run<T: Send + 'static, E: From<DbError> + CarriesBug + Send + 'static>(
79 &self,
80 run: impl FnOnce(&mut DatabaseConnection, &WriteSchema, &StoreDir, DeviceId) -> Result<T, E>
81 + Send
82 + 'static,
83 ) -> Result<T, E> {
84 self.database
85 .call(move |open| {
86 let access = M::access(open);
87 access.with_writer(|writer| {
88 run(writer, &access.write_schema, &access.directory, access.device)
89 })
90 })
91 .await
92 }
93
94 pub async fn file_ref(&self, file: FileId) -> Result<FileRef, DbError> {
96 self.database.file_ref(file).await
97 }
98 pub async fn open_local(
102 &self,
103 file: &FileRef,
104 ) -> Result<Option<crate::LocalFileStream>, crate::LocalFileError> {
105 self.database.open_local_file(file).await
106 }
107
108 pub async fn cached(&self, file: &FileRef, index: i64) -> Result<Option<Vec<u8>>, DbError> {
111 let file = file.clone();
112 self.database.call(move |open| cache::read(M::access(open), &file, index)).await
113 }
114 pub async fn repair_cached(
118 &self,
119 file: &FileRef,
120 index: i64,
121 bytes: Vec<u8>,
122 ) -> Result<(), DbError> {
123 let file = file.clone();
124 self.database.call(move |open| cache::repair(M::access(open), &file, index, &bytes)).await
125 }
126 pub async fn cache_range(
132 &self,
133 file: &FileRef,
134 first: i64,
135 chunks: i64,
136 name: FileName,
137 bytes: Vec<u8>,
138 ) -> Result<(), DbError> {
139 let file = file.clone();
140 self.database
141 .call(move |open| cache::put(M::access(open), &file, first, chunks, &name, &bytes))
142 .await
143 }
144 async fn remove_released(
148 &self,
149 chosen: crate::file_removals::Chosen,
150 ) -> Result<Vec<DbError>, DbError> {
151 self.database
152 .call(move |open| Ok::<_, DbError>(M::access(open).remove_released(chosen)))
153 .await
154 }
155 async fn removed(&self, chosen: crate::file_removals::Chosen) -> Result<(), DbError> {
156 let failures = self.remove_released(chosen).await?;
157 crate::write_failure::after_cleanup(Ok(()), failures)
158 }
159 pub async fn trim_cache(&self, namespace: &str) -> Result<(), DbError> {
161 let namespace = namespace.to_owned();
162 let chosen = self
163 .run(move |db, _, _, _| {
164 let paths = cache::trim(db, &namespace)?;
165 Ok::<_, DbError>(crate::file_removals::choose_cache(&paths))
166 })
167 .await?;
168 self.removed(chosen).await
169 }
170 #[cfg(any(test, feature = "test-utils"))]
174 pub async fn missing_bytes(&self, file: &FileRef) -> Result<u64, DbError> {
175 let file = file.clone();
176 self.run(move |db, _, _, device| {
177 Ok(file.size() - crate::cache_progress::covered(db, &file, device)?)
178 })
179 .await
180 }
181 pub async fn budget(&self, namespace: &str) -> Result<u64, DbError> {
183 let namespace = namespace.to_owned();
184 self.run(move |db, _, _, _| cache::budget(db, &namespace)).await
185 }
186}
187
188impl FileDatabase {
189 pub fn uploads_in_flight(&self) -> crate::InFlight {
192 self.database.uploads_in_flight()
193 }
194
195 pub fn downloads_in_flight(&self) -> crate::InFlight {
198 self.database.downloads_in_flight()
199 }
200
201 async fn read<T: Send + 'static, E: From<DbError> + CarriesBug + Send + 'static>(
204 &self,
205 read: impl FnOnce(&DatabaseConnection) -> Result<T, E> + Send + 'static,
206 ) -> Result<T, E> {
207 self.database.call(move |inner| inner.access.with_committed(read)).await
208 }
209 pub async fn local_evidence<P: crate::PendingValue>(
214 &self,
215 file: &FileRef,
216 upload_subject: impl FnOnce(&crate::FileLocation) -> P::Subject + Send + 'static,
217 move_subject: impl FnOnce(crate::MoveId) -> P::Subject + Send + 'static,
218 ) -> Result<Option<LocalEvidence<P>>, DbError>
219 where
220 P::Subject: Send,
221 {
222 let file = file.clone();
223 self.run(move |db, _, _, device| {
224 db.read_transaction(|| {
225 if file.place() != (crate::FilePlace::DeviceOnly { device }) {
226 return Ok(None);
227 }
228 let Some(upload) = file_queue::upload_of(db, file.id())? else {
229 return Ok(None);
230 };
231 let (location, of) = db.query_row(
232 "SELECT location,move FROM _coven_file_uploads WHERE id=?1",
233 [upload],
234 |r| Ok((r.get::<_, String>(0)?, r.get::<_, crate::MoveId>(1)?)),
235 )?;
236 let location = crate::FileLocation::decode(&location)?;
237 let record = crate::pending::find::<P>(db, &upload_subject(&location))?;
238 let moving = crate::pending::find::<P>(db, &move_subject(of))?;
239 let original = crate::user_file::read(db, file.id())?.map(|original| original.path);
240 Ok(Some(LocalEvidence { upload, location, of, original, record, moving }))
241 })
242 })
243 .await
244 }
245 pub async fn fail_move<P: crate::PendingValue>(
251 &self,
252 upload: i64,
253 record: P,
254 ) -> Result<(), DbError> {
255 self.run(move |db, _, _, _| {
256 db.state_transaction(|db| {
257 let Some(of) = record.move_of() else {
258 return Err(DbError::Invariant {
259 what: "a failed move's record names the move",
260 });
261 };
262 if crate::file_moves::of_upload(db, upload)? != of {
263 return Err(DbError::Invariant {
264 what: "a move fails through one of its own uploads",
265 });
266 }
267 if crate::file_moves::state(db, of)? != Some(crate::file_moves::MoveState::Moving) {
268 return Ok(());
269 }
270 crate::file_moves::end_unswitched(db, of, crate::file_moves::MoveState::Failed)?;
271 crate::pending::save(db, &record)
272 })
273 })
274 .await
275 }
276 pub async fn clear_original<P: crate::PendingValue>(
282 &self,
283 upload: i64,
284 subject: P::Subject,
285 checked: impl Fn(&P, &std::path::Path) -> bool + Send + 'static,
286 ) -> Result<Option<std::path::PathBuf>, DbError>
287 where
288 P::Subject: Send + 'static,
289 {
290 self.run(move |db, _, _, _| {
291 db.state_transaction(|db| {
292 let original: Option<Option<Vec<u8>>> = db
293 .query("SELECT original FROM _coven_file_uploads WHERE id=?1", [upload], |r| {
294 r.get(0)
295 })?
296 .pop();
297 let Some(Some(original)) = original else {
298 return Ok(None);
299 };
300 let path = crate::user_file::decode_path(original)?;
301 if crate::pending::find::<P>(db, &subject)?
302 .is_some_and(|record| checked(&record, &path))
303 {
304 crate::pending::clear(db, &subject)?;
305 crate::retry::clear(db, crate::RetrySubject::Upload(upload))?;
306 return Ok(Some(path));
307 }
308 Ok(None)
309 })
310 })
311 .await
312 }
313 pub async fn retire_unlinked<S: serde::Serialize + Send + 'static>(
319 &self,
320 in_flight: std::collections::BTreeSet<ObjectId>,
321 subject: impl Fn(&crate::FileLocation) -> S + Send + 'static,
322 ) -> Result<Vec<i64>, DbError> {
323 let database = &self.database;
325 let work = database
326 .call(move |inner| {
327 inner
328 .access
329 .with_committed(|db| Ok::<_, DbError>(!file_queue::retirable(db)?.is_empty()))
330 })
331 .await?;
332 if !work {
333 return Ok(Vec::new());
334 }
335 database
336 .call(move |inner| {
337 inner.with_files("retire uploads", |writer, files| {
341 writer.local_write(
342 &inner.access.write_schema,
343 inner.access.device,
344 inner.clock.now(),
345 files,
346 |files| files.retire_unlinked(&in_flight, subject),
347 )
348 })
349 })
350 .await
351 }
352 pub async fn fail_download<P: crate::PendingValue>(&self, record: P) -> Result<(), DbError> {
356 self.database.record_pending(record).await
357 }
358 pub async fn clear_download_pending<P: crate::PendingValue>(
361 &self,
362 subject: P::Subject,
363 checked: impl Fn(&P) -> bool + Send + 'static,
364 ) -> Result<(), DbError>
365 where
366 P::Subject: Send + 'static,
367 {
368 self.database.clear_checked::<P>(subject, checked).await
369 }
370 pub async fn clear_upload_pending<P: crate::PendingValue>(
375 &self,
376 object: (DeviceId, ObjectId),
377 subject: P::Subject,
378 checked: impl Fn(&P) -> bool + Send + 'static,
379 ) -> Result<Option<crate::RetrySubject>, DbError>
380 where
381 P::Subject: Send + 'static,
382 {
383 self.database.clear_pending_if::<P>(object, subject, checked).await
384 }
385 pub async fn fail_local_read<P, E>(
391 &self,
392 file: FileId,
393 record: P,
394 next: impl FnOnce(Option<crate::RetryWait>) -> Result<crate::RetryWait, E> + Send + 'static,
395 ) -> Result<Option<(crate::RetrySubject, crate::RetryWait)>, E>
396 where
397 P: crate::PendingValue,
398 E: From<DbError> + CarriesBug + Send + 'static,
399 {
400 self.run(move |db, _, _, _| {
401 db.state_transaction(|db| {
402 let Some(id) = file_queue::upload_of(db, file)? else {
403 return Ok(Ok(None));
404 };
405 let subject = crate::RetrySubject::Upload(id);
406 let wait = match next(crate::retry::find(db, subject)?) {
409 Ok(wait) => wait,
410 Err(refused) => return Ok(Err(refused)),
411 };
412 crate::pending::save(db, &record)?;
413 crate::retry::save(db, subject, &wait)?;
414 Ok(Ok(Some((subject, wait))))
415 })
416 })
417 .await?
418 }
419
420 pub async fn uploads(&self) -> Result<Vec<FileUpload>, DbError> {
422 self.read(file_queue::read).await
423 }
424 #[cfg(any(test, feature = "test-utils"))]
426 pub async fn all_uploads(&self) -> Result<Vec<FileUpload>, DbError> {
427 self.read(file_queue::read_all).await
428 }
429 pub async fn bound_uploads(&self) -> Result<std::collections::BTreeSet<i64>, DbError> {
432 self.read(file_queue::bound).await
433 }
434 pub async fn uploads_paused(&self) -> Result<bool, DbError> {
436 self.read(file_queue::paused).await
437 }
438 pub async fn writes_paused(&self) -> Result<bool, DbError> {
441 self.read(crate::operation::writes_paused).await
442 }
443 pub async fn unmetered_only(&self) -> Result<bool, DbError> {
446 self.read(file_queue::unmetered_only).await
447 }
448 pub async fn upload_work<P: crate::PendingValue>(
452 &self,
453 subject: impl Fn(&crate::FileLocation) -> P::Subject + Send + 'static,
454 ) -> Result<Vec<(FileUpload, Option<P>)>, DbError> {
455 self.run(move |db, _, _, _| {
456 db.read_transaction(|| {
457 file_queue::read(db)?
458 .into_iter()
459 .filter(|upload| !upload.stored)
461 .map(|upload| {
462 let record = crate::pending::find::<P>(db, &subject(&upload.location))?;
463 Ok((upload, record))
464 })
465 .collect()
466 })
467 })
468 .await
469 }
470 pub async fn abandoned_uploads<P: crate::PendingValue>(
474 &self,
475 subject: impl Fn(&crate::FileLocation) -> P::Subject + Send + 'static,
476 ) -> Result<Vec<(FileUpload, Option<P>)>, DbError> {
477 self.run(move |db, _, _, _| {
478 db.read_transaction(|| {
479 file_queue::abandoned(db)?
480 .into_iter()
481 .map(|upload| {
482 let record = crate::pending::find::<P>(db, &subject(&upload.location))?;
483 Ok((upload, record))
484 })
485 .collect()
486 })
487 })
488 .await
489 }
490 pub async fn forget_session(&self, id: i64) -> Result<(), DbError> {
495 self.run(move |db, _, _, _| db.transaction(|db| crate::file_queue::forget_session(db, id)))
496 .await
497 }
498 pub async fn fail_upload<P: crate::PendingValue>(
502 &self,
503 id: i64,
504 record: P,
505 retry: Option<crate::RetryWait>,
506 ) -> Result<(), DbError> {
507 self.run(move |db, _, _, _| {
508 db.transaction(|db| {
509 if !db.query_row(
510 "SELECT EXISTS(SELECT 1 FROM _coven_file_uploads WHERE id=?1)",
511 [id],
512 |r| r.get::<_, bool>(0),
513 )? {
514 return Err(DbError::Invariant {
515 what: "the upload recording progress is queued",
516 });
517 }
518 crate::retry::save_failure(
519 db,
520 &record,
521 crate::RetrySubject::Upload(id),
522 retry.as_ref(),
523 )
524 })
525 })
526 .await
527 }
528 pub async fn upload_chunk_hash(
531 &self,
532 location: &crate::FileLocation,
533 index: u64,
534 ) -> Result<coven_crypto::ContentHash, DbError> {
535 let location = location.clone();
536 self.read(move |db| file_queue::chunk_hash(db, &location, index)).await
537 }
538 pub async fn open_upload_source(
542 &self,
543 upload: &FileUpload,
544 ) -> Result<Option<crate::LocalFileStream>, crate::LocalFileError> {
545 let (id, object, size) = (upload.id, upload.location.object(), upload.size);
546 self.database.call(move |inner| open_upload_source(&inner.access, id, object, size)).await
547 }
548
549 #[expect(
554 clippy::map_err_ignore,
555 reason = "a byte count past SQLite's integers breaks the named invariant"
556 )]
557 pub async fn record_session(
558 &self,
559 id: i64,
560 session: SecretBytes,
561 progress: crate::UploadProgress,
562 ) -> Result<(), DbError> {
563 let count = |bytes: u64| {
564 i64::try_from(bytes)
565 .map_err(|_| DbError::Invariant { what: "an upload's size fits SQLite" })
566 };
567 let confirmed = count(progress.bytes_confirmed)?;
568 let total = count(progress.bytes_total)?;
569 self.run(move |db, _, _, _| {
570 db.transaction(|db| {
571 changed(crate::file_queue::record_session(
572 db,
573 id,
574 session.as_bytes(),
575 confirmed,
576 total,
577 )?)
578 })
579 })
580 .await
581 }
582 pub async fn complete_upload<S: serde::Serialize + Send + 'static>(
592 &self,
593 id: i64,
594 subject: S,
595 ) -> Result<crate::UploadCompletion, DbError> {
596 let subject = crate::pending::SubjectKey::of(&subject)?;
597 self.database.call(move |inner| complete(inner, id, subject)).await
598 }
599 pub async fn reserve_cache_file(
602 &self,
603 file: &FileRef,
604 name: FileName,
605 ) -> Result<CacheReservation, DbError> {
606 let database = &self.database;
607 let writing = BytesWriting::of(database).await;
608 let file = file.clone();
609 database
610 .call(move |inner| {
611 inner.access.with_writer(|writer| {
612 writer.transaction(|db| {
613 if db.query_row(
614 &format!("SELECT {CACHE_NAME_USED_SQL}"),
615 [name.as_str()],
616 |r| r.get::<_, bool>(0),
617 )? {
618 return Err(DbError::FileNameReused { name: name.as_str().to_owned() });
619 }
620 crate::file_removals::record_cached(db, name.as_str())?;
621 Ok(())
622 })?;
623 inner.staging.reserve([crate::file_removals::OwnedFile::Cached(name.clone())]);
624 Ok::<_, DbError>(())
625 })?;
626 Ok(CacheReservation {
627 reserved: inner.staging.clone(),
628 file,
629 name,
630 writing: Some(writing),
631 partial: false,
632 })
633 })
634 .await
635 }
636
637 pub fn queued_work_changes(&self) -> DatabaseChanges {
641 let reads = vec![
646 crate::observation::TableRead {
647 table: "_coven_write_pause".into(),
648 columns: ["paused".into()].into(),
649 scope: crate::row_scope::RowScope::All,
650 inserted_only: false,
651 },
652 crate::observation::TableRead {
653 table: "_coven_file_uploads".into(),
654 columns: ["location".into()].into(),
655 scope: crate::row_scope::RowScope::All,
656 inserted_only: true,
657 },
658 crate::observation::TableRead {
659 table: "_coven_upload_files".into(),
660 columns: ["file".into()].into(),
661 scope: crate::row_scope::RowScope::All,
662 inserted_only: false,
663 },
664 ];
665 self.database.observer.changes(reads, true)
666 }
667}
668
669pub struct CacheReservation {
672 reserved: Arc<crate::reserved_names::ReservedNames>,
674 file: FileRef,
675 name: FileName,
676 writing: Option<BytesWriting>,
678 partial: bool,
680}
681impl CacheReservation {
682 pub fn name(&self) -> &FileName {
684 &self.name
685 }
686}
687struct BytesWriting {
694 _lease: tokio::sync::OwnedRwLockReadGuard<()>,
695 _store: Arc<std::sync::RwLock<Option<DatabaseInner>>>,
696}
697impl BytesWriting {
698 async fn of(database: &Database) -> Self {
701 Self {
702 _lease: database.writer.file_tasks.clone().read_owned().await,
703 _store: database.inner.clone(),
704 }
705 }
706}
707impl Drop for CacheReservation {
708 fn drop(&mut self) {
709 let Some(_writing) = self.writing.take() else {
711 return;
712 };
713 self.reserved.release([crate::file_removals::OwnedFile::Cached(self.name.clone())]);
714 }
715}
716fn complete(
719 inner: &DatabaseInner,
720 id: i64,
721 subject: crate::pending::SubjectKey,
722) -> Result<crate::UploadCompletion, DbError> {
723 inner.with_files("complete an upload", |writer, files| {
724 writer.local_write(
725 &inner.access.write_schema,
726 inner.access.device,
727 inner.clock.now(),
728 files,
729 |files| {
730 files.complete_upload(
731 id,
732 Box::new(move |db| crate::pending::clear_key(db, &subject)),
733 )
734 },
735 )
736 })
737}
738fn open_upload_source<L>(
741 access: &DatabaseAccess<L>,
742 upload: i64,
743 object: ObjectId,
744 size: u64,
745) -> Result<Option<crate::LocalFileStream>, crate::LocalFileError> {
746 let source = access.with_writer(|db| file_ref::queued_source(db, upload))?;
747 source
748 .map(|source| file_ref::open_source(&access.directory, &object.to_string(), size, source))
749 .transpose()
750}
751
752impl Database {
753 pub async fn settle_files(&self) -> Result<(), DbError> {
758 self.call(move |inner| {
759 let waiting = inner.access.with_committed(|db| {
760 db.query_row(
761 "SELECT EXISTS(SELECT 1 FROM _coven_unreferenced_files)
762 AND NOT EXISTS(SELECT 1 FROM _coven_write_pause)",
763 [],
764 |r| r.get::<_, bool>(0),
765 )
766 })?;
767 if !waiting {
768 return Ok(());
769 }
770 inner.with_files("settle files", |writer, files| {
771 writer.local_write(
772 &inner.access.write_schema,
773 inner.access.device,
774 inner.clock.now(),
775 files,
776 |files| files.settle(),
777 )
778 })
779 })
780 .await
781 }
782}
783
784fn changed(count: usize) -> Result<(), DbError> {
785 if count == 1 {
786 Ok(())
787 } else {
788 Err(DbError::Invariant { what: "a queued upload's update changes exactly its row" })
789 }
790}
791
792#[derive(Debug)]
795pub struct LocalEvidence<P> {
796 pub upload: i64,
798 pub location: crate::FileLocation,
800 pub of: crate::MoveId,
802 pub original: Option<std::path::PathBuf>,
804 pub record: Option<P>,
806 pub moving: Option<P>,
808}
809
810#[path = "file_database_pins.rs"]
811mod pins;
812
813#[path = "file_database_partial_pins.rs"]
814mod partial_pins;
815pub(crate) use partial_pins::remove as remove_partial_pin;
816
817#[path = "file_database_prepared_pin.rs"]
818mod prepared_pin;
819pub use prepared_pin::PreparedPin;
820
821#[path = "file_database_copies.rs"]
822pub(crate) mod copies;
823pub use copies::{CopyReservation, FolderCopy, PreparedDeviceOnly};
824
825#[cfg(test)]
826#[path = "file_database_tests.rs"]
827pub(crate) mod tests;