Skip to main content

coven_database/
file_database.rs

1//! File metadata operations through the owning database, never through app SQL.
2
3use 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
26/// File queue and cache access, on the store's writer or a read-only
27/// handle (`FileDatabase<ReadOnly>`), which reads and caches files and can
28/// change only its local cache metadata. The upload and pin queue and the
29/// file records are the writer's. Construct beside the database at the
30/// application composition root.
31///
32/// ```compile_fail
33/// async fn completes_nothing(files: &coven_database::FileDatabase<coven_database::ReadOnly>) {
34///     files.complete_upload(1, ()).await;
35/// }
36/// ```
37/// ```compile_fail
38/// fn queues_nothing(files: &coven_database::FileDatabase<coven_database::ReadOnly>) {
39///     files.queued_work_changes();
40/// }
41/// ```
42/// ```compile_fail
43/// async fn records_nothing(files: &coven_database::FileDatabase<coven_database::ReadOnly>) {
44///     files.fail_upload(1, (), None).await;
45/// }
46/// ```
47pub struct FileDatabase<M: Mode = Writer> {
48    database: Database<M>,
49}
50
51// Written out: `derive(Clone)` would ask the mode to be `Clone`.
52impl<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    /// Use an open store's handle for its file operations.
60    pub fn new(database: Database<M>) -> Self {
61        Self { database }
62    }
63
64    /// Where a bug in the file owner's work that nobody waits for goes, as
65    /// its database's does (E15).
66    pub fn bugs(&self) -> &coven_foundation::task::BugSinkRef {
67        self.database.bugs()
68    }
69
70    /// The writer's file owner, which keeps file records and the upload and
71    /// pin queue; a read-only handle keeps none.
72    pub fn records(&self) -> Option<&FileDatabase<Writer>> {
73        M::records(self)
74    }
75
76    /// Run `run` on the handle's writable connection: the store's writer,
77    /// or a read-only handle's cache connection.
78    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    /// The file this device has a record of under this id.
95    pub async fn file_ref(&self, file: FileId) -> Result<FileRef, DbError> {
96        self.database.file_ref(file).await
97    }
98    /// Open and check the local source of a file still on this device only,
99    /// while it uploads. `None` means its bytes come from the cache or
100    /// storage.
101    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    /// Cached encrypted chunk or header (-1). Its bytes are read with no
109    /// writer held (§21.2); an entry evicted meanwhile reads as not cached.
110    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    /// Write the stored bytes of chunk `index`, or the header (-1), back
115    /// over the cached copy of them that failed its check (§17.8). The
116    /// bytes are written with no writer held (§21.2).
117    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    /// Keep one verified range of `chunks` consecutive encrypted chunks from
127    /// `first`, or the header (`first` = -1), durably before publishing its
128    /// cache metadata in one transaction. The bytes are written with no
129    /// writer held (§21.2), on a blocking worker that finishes even if the
130    /// caller stops waiting.
131    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    /// Remove the bytes of cache entries the writer just chose, once it is
145    /// free (§21.2, Appendix C.3). Returns the failures, whose records stay for the
146    /// next attempt.
147    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    /// Evict unpinned chunks in least-recently-read order, independently per namespace.
160    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    /// Plaintext bytes absent from this namespace's cache; headers and tags
171    /// are excluded from application download progress. A file only on this
172    /// device is wholly local.
173    #[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    /// The namespace's budget, declared or since set.
182    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    /// The files this process is sending now, which the upload worker marks
190    /// as each attempt starts and ends (§17.9).
191    pub fn uploads_in_flight(&self) -> crate::InFlight {
192        self.database.uploads_in_flight()
193    }
194
195    /// The files this process is downloading to keep whole now, which each
196    /// pin's download marks as it starts and ends (§17.8).
197    pub fn downloads_in_flight(&self) -> crate::InFlight {
198        self.database.downloads_in_flight()
199    }
200
201    /// A read-only query on one committed state, on the read pool, so
202    /// read-only decisions never take the writer (§21.2).
203    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    /// The upload of a file this device uploads, its move, this device's
210    /// record about the upload's bytes and its move's record, read in one
211    /// committed state: the local facts its status is judged with (§17.6).
212    /// `None` when this device doesn't upload the file.
213    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    /// Fail a move because a user's original changed before its upload
246    /// (§17.3): none of its files switches, each stays on this device only,
247    /// and its uploads retire, their stored bytes becoming deletion
248    /// candidates. `record` is the move's own and says which original. A
249    /// move that already ended keeps its state.
250    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    /// Clear the record `subject` names if `checked` says the read or upload
277    /// of `upload`'s original that just succeeded settles it, by the record
278    /// and the original's path. The predicate and deletion share the
279    /// pending transaction, which also ends the upload's retry wait the
280    /// failure armed. Returns the original's path when it cleared.
281    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    /// Retire the uploads no file waits for any more, except those whose
314    /// objects are in `in_flight`, whose attempts retire them as they end
315    /// (§17.9). Each retired upload's pending record goes with it, and its
316    /// stored path becomes a deletion candidate unless something else
317    /// protects it (§17.10). Returns the retired queue ids.
318    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        // An idle run writes nothing: only work found takes the writer.
324        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                // The released copies go once the writer is free (§21.2). A
338                // move whose last waiting upload retires switches its files
339                // in this write (§17.3).
340                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    /// Record this device's first unmet condition for the stored bytes a
353    /// pin downloads, replacing an earlier reason. It keeps no wait: only the
354    /// app's next pin or read tries again (§17.7).
355    pub async fn fail_download<P: crate::PendingValue>(&self, record: P) -> Result<(), DbError> {
356        self.database.record_pending(record).await
357    }
358    /// Clear only conditions the completed download actually checked. The
359    /// predicate and deletion share the pending transaction.
360    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    /// Clear only conditions the completed local read actually checked. The
371    /// predicate and deletion share the pending transaction. Any queued
372    /// upload wait ends with it; return the upload subject so the caller can
373    /// discard its in-memory timer.
374    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    /// Record a local source failure only while its upload remains queued.
386    /// The retry is selected from the current upload wait and committed with
387    /// the record, so concurrent upload completion cannot leave a stale failure.
388    /// A `next` that fails records nothing and returns its own error; only a
389    /// failed save fails the handle, as any lost pending state does (E5).
390    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                // Nothing is written yet: a refused wait leaves the
407                // transaction with nothing to commit.
408                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    /// Durable queue: each upload some file still waits for.
421    pub async fn uploads(&self) -> Result<Vec<FileUpload>, DbError> {
422        self.read(file_queue::read).await
423    }
424    /// Every queued upload, those no file waits for included.
425    #[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    /// The queue ids of the bound uploads, which the pause leaves running
430    /// (§17.4).
431    pub async fn bound_uploads(&self) -> Result<std::collections::BTreeSet<i64>, DbError> {
432        self.read(file_queue::bound).await
433    }
434    /// Whether the app paused uploads, as persisted (E7).
435    pub async fn uploads_paused(&self) -> Result<bool, DbError> {
436        self.read(file_queue::paused).await
437    }
438    /// Whether a self-replacement pauses this device's writes, which holds
439    /// a trailing upload's completion until it ends (§12.3, §17.4).
440    pub async fn writes_paused(&self) -> Result<bool, DbError> {
441        self.read(crate::operation::writes_paused).await
442    }
443    /// Whether the app moves files only on unmetered networks, as persisted
444    /// (E7).
445    pub async fn unmetered_only(&self) -> Result<bool, DbError> {
446        self.read(file_queue::unmetered_only).await
447    }
448    /// Each queued upload with this device's pending record about its file,
449    /// read in one committed state: the reason an earlier attempt left decides
450    /// whether automatic work retries it (§17.9).
451    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                    // A stored upload only waits for the rest of its move.
460                    .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    /// Each upload no file waits for any more that still records a provider
471    /// session, with this device's pending record about its file, read in one
472    /// committed state: its session is aborted before it retires (§17.9).
473    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    /// Forget the session of upload `id` once storage has aborted it, if no
491    /// file waits for the upload: it then retires with the others (§17.9).
492    /// One a file waits for again keeps its record, and its next attempt
493    /// finds the session gone and starts another.
494    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    /// Record a failed attempt as this device's pending record about its file,
499    /// with its retry wait or none when it waits for a prerequisite or the app,
500    /// in one transaction (§17.9, §19.1).
501    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    /// Read the first-read plaintext hash of one queued chunk, keyed by the
529    /// upload's location and owned by the queue whatever its files became.
530    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    /// Open the source an upload sends, from the facts its queue entry fixed
539    /// at staging; later row changes cannot replace them. `None` means the
540    /// upload is no longer queued. It is opened with no writer held.
541    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    /// Persist the provider recording after each confirmed change in progress,
550    /// with the bytes it records as confirmed, which `coven_uploads` gives the
551    /// app (E4). Storage alone reads the recording, so the caller gives both
552    /// from the same session.
553    #[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    /// Complete an upload once storage confirms its complete bytes (§17.9):
583    /// one local transaction switches each file the upload stores to the
584    /// cloud, as this device's write, and removes the queue row, its chunk
585    /// hashes, the copy kept for it and this device's pending record about
586    /// it. A file gone meanwhile switches nothing. Afterwards this device
587    /// reads the files from the cache or storage. A write that can't be made
588    /// now fails the transaction, and the upload stays queued. An upload no
589    /// file waits for any more, of a move that failed or was cancelled while
590    /// it was sent, retires instead, storing no file.
591    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    /// Reserve a whole cache file of `file` while it is written and checked,
600    /// for a pin; a read-only handle caches individual chunks only.
601    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    /// Observe the upload worker's work: newly queued uploads, and uploads a
638    /// file stopped waiting for. Subscribing before reading the work list
639    /// prevents missed commits.
640    pub fn queued_work_changes(&self) -> DatabaseChanges {
641        // A newly queued upload, an upload a file stopped waiting for, which
642        // the worker then retires (§17.9), and a self-replacement's write
643        // pause starting or ending, which holds trailing uploads'
644        // completions (§12.3, §17.4).
645        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
669/// Bytes reserved for one file's whole encrypted cache file while it is
670/// written, until a prepared pin takes it (§17.8).
671pub struct CacheReservation {
672    /// The names the store that reserved this file reserves.
673    reserved: Arc<crate::reserved_names::ReservedNames>,
674    file: FileRef,
675    name: FileName,
676    /// Held until a prepared pin takes the copy or it drops.
677    writing: Option<BytesWriting>,
678    /// Whether the file is kept as a partial download, its row naming it.
679    partial: bool,
680}
681impl CacheReservation {
682    /// The cache file's name.
683    pub fn name(&self) -> &FileName {
684        &self.name
685    }
686}
687/// What a reservation holds while its bytes are still written, a pin's
688/// whole cache file or a device-only call's copies: the lease closing the
689/// store waits for, and the open store, so that once its last handle drops
690/// the store stays closing, and a reopen waits, until the writing ends.
691/// Opening removes recorded bytes, and would delete bytes this writer still
692/// owns (Appendix C.3, F.4).
693struct BytesWriting {
694    _lease: tokio::sync::OwnedRwLockReadGuard<()>,
695    _store: Arc<std::sync::RwLock<Option<DatabaseInner>>>,
696}
697impl BytesWriting {
698    /// Hold `database`'s store while bytes are written, once no close has
699    /// begun waiting for its file work.
700    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        // The name goes before the store the writing holds can close.
710        let Some(_writing) = self.writing.take() else {
711            return;
712        };
713        self.reserved.release([crate::file_removals::OwnedFile::Cached(self.name.clone())]);
714    }
715}
716/// One upload's completion (§17.9): a local write that switches the files
717/// the upload stores to the cloud, then retires the upload.
718fn 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}
738/// Find a queued upload's source under the writer, then open it once the
739/// writer is free: opening is file IO (§21.2).
740fn 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    /// Settle what other devices' writes left for this device's files, in
754    /// one write of its own (§17.2): delete each record they left with no
755    /// reference. An audience reloading, or a pause of this device's writes,
756    /// leaves its files for a later call.
757    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/// What this device knows locally about a file it uploads (§17.6): the
793/// upload, where its bytes go, its move, and the records about each.
794#[derive(Debug)]
795pub struct LocalEvidence<P> {
796    /// The upload's queue id.
797    pub upload: i64,
798    /// Where its bytes go, with their key.
799    pub location: crate::FileLocation,
800    /// The move it belongs to.
801    pub of: crate::MoveId,
802    /// The file's original, for a user's file.
803    pub original: Option<std::path::PathBuf>,
804    /// This device's record about the upload's bytes.
805    pub record: Option<P>,
806    /// The move's record.
807    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;